commit 8e7bef13c52af3449952514148c6375280547365 Author: ipatini Date: Fri Aug 11 09:31:47 2023 +0300 Merge branch 'ems/prepare-for-nebulous' diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e0e65d7 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..768b37c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr diff --git a/README.md b/README.md new file mode 100644 index 0000000..8eb19dd --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +repo init diff --git a/ems-core/.gitattributes b/ems-core/.gitattributes new file mode 100644 index 0000000..b859111 --- /dev/null +++ b/ems-core/.gitattributes @@ -0,0 +1,9 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +*.sh text eol=lf \ No newline at end of file diff --git a/ems-core/.gitignore b/ems-core/.gitignore new file mode 100644 index 0000000..c2f5d82 --- /dev/null +++ b/ems-core/.gitignore @@ -0,0 +1,10 @@ +.idea +broker-client/*.js +config-files/*.p12 +config-files/*.crt +public_resources/** +tcnative* +ems.log +server.pem +.dev-* +.flattened-pom.xml \ No newline at end of file diff --git a/ems-core/README-for-TESTING.md b/ems-core/README-for-TESTING.md new file mode 100644 index 0000000..0337523 --- /dev/null +++ b/ems-core/README-for-TESTING.md @@ -0,0 +1,1328 @@ +# Testing of New EMS Features + + +## New features of EMS + +- Support for **Resource-Limited (RL)** nodes, like edge devices or small VMs +- Support for **Self-Healing** monitoring topology (partially implemented) + + +## Definitions +We distinguish between ***Resource-Limited (RL)*** nodes and ***Normal or Non-RL*** nodes. + +- **Normal nodes** are VMs have enough resources, where an EMS client will be installed, along with JRE and Netdata. +- **RL nodes** are VMs with few resources, where only Netdata will be installed. +- Currently, EMS will classify a VM as an RL node if: + * it has 1 or 2 cores, or + * it has 2GB of RAM or less, or + * it has Total Disk space 1GB or less, or + * its architecture name starts with `ARM` (it will normally be `x86_64`). + * Thresholds can be changed in `gr.iccs.imu.ems.baguette-client-install.properties` file. + + +We also distinguish between ***Monitoring Topologies***: + +- **2-LEVEL Monitoring Topology**: Nodes send their metrics directly to EMS server. + + * Includes an EMS server, and any number of Normal and/or RL nodes. + * No clustering occurs in 2-LEVEL topologies, hence Aggregator role is not used. + * CAMEL Metric Models will only use `GLOBAL` and `PER_INSTANCE` groupings or no groupings at all (`GLOBAL` and `PER_INSTANCE` are then implied). + +- **3-LEVEL Monitoring Topology**: Nodes send their metrics to cluster-wide Aggregators, then Aggregators send (composite) metrics to EMS server. + + * Includes an EMS server, Aggregators (one per cluster), and Normal and/or RL nodes. + * Nodes are groupped into clusters. Each cluster has a node with the Aggregator role. + * Only Normal nodes can be Aggregators. + * There must be exactly one Aggregator per cluster. + * Each cluster must have at least one Normal node (in order to become Aggregator). + * CAMEL Metric Model will use `GLOBAL`, `PER_ZONE` / `PER_REGION` / `PER_CLOUD`, and `PER_INSTANCE` groupings. + + Clustering of nodes is used for faster failure detection, as well as distribution of load: + - Only 3-LEVEL topologies are clustered. + - 2-LEVEL topologies are not clustered. + + Currently, nodes are clustered based on their: + - Availability Zone or Region or Cloud Service Provider, or + - assigned to a default cluster. + + +------ + + +## A) Support for Resource-Limited nodes +> Feature Quick Notes: +> - EMS server will NOT install EMS client and JRE in RL nodes. +> - EMS server will install Netdata in RL nodes. +> - EMS server or an Aggregator will periodically query Netdata agents of RL nodes for metrics. +> - Normal nodes will periodically query their Local Netdata agent for metrics. + + + +### Test Cases + +**A.1) Metrics collection from RL nodes in a 2-LEVEL topology** + +> Test Case Quick Notes: +> - EMS server MUST log when it collects metrics from RL nodes. +> - EMS server MUST *NOT* log or collect metrics from Normal (Non-RL) nodes. +> - Normal nodes MUST log when they collect metrics from their Local Netdata agents. (The Log records are slightly different). + +**You need a CAMEL model:** + +* with two Requirement Sets: + - for Normal nodes: 4 cores, 4GB RAM, >1 GB Disk, and + - for RL nodes: 1-2 cores, or <2GB RAM, or <1GB Disk +* with 1-2 COMPONENTS using Requirement Set #1 (Normal nodes) +* with 1-2 COMPONENTS with Requirement Set #2 (RL nodes) +* with no Groupings in Metric Model + +**After Application deployment you need to check the logs of:** + +* ***EMS server***, for log messages about collecting metrics from RL-nodes' Netdata agents. E.g. + + ``` + e.m.e.c.c.netdata.NetdataCollector : Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.32.2, 192.168.32.4] + e.m.e.c.c.netdata.NetdataCollector : Collectors::Netdata: Collecting data from url: http://192.168.32.2:19999/api/v1/allmetrics?format=json + e.m.e.c.c.netdata.NetdataCollector : Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + e.m.e.c.c.netdata.NetdataCollector : Collectors::Netdata: Collecting data from url: http://192.168.32.4:19999/api/v1/allmetrics?format=json + e.m.e.c.c.netdata.NetdataCollector : Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + +* ***Normal nodes***, for log messages about collecting metrics from their Local Netdata agent + + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + + + +**A.2) Metrics collection from RL nodes in a 3-LEVEL topology** + +> Test Case Quick Notes: +> - The Aggregator (it is a Normal node) MUST log each time it collects metrics from RL nodes in its cluster. +> - The Aggregator MUST *NOT* log or collect metrics from Normal (Non-RL) nodes in its cluster. +> - Normal nodes (including Aggregator) MUST log each time they collect metrics from their Local Netdata agents. (The Log records are slightly different). + +**You need a CAMEL model:** + +* with two Requirement Sets: + - for Normal nodes: 4 cores, 4GB RAM, >1 GB Disk, and + - for RL nodes: 1-2 cores, or <2GB RAM, or <1GB Disk +* with 1-2 COMPONENTS with Requirement Set #1 (Normal nodes) +* with 1-2 COMPONENTS with Requirement Set #2 (RL nodes) +* with three (3) Groupings used in the Metric Model (`GLOBAL`, `PER_ZONE`, `PER_INSTANCE`) + +**After Application deployment you need to check the logs of:** + +* ***EMS server***, for NO logs related collecting metrics from any Netdata agent +* ***Aggregator node(s)***, for logs about collecting metrics from the Netdata agents of RL nodes, in the same cluster. E.g. + + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2, 192.168.96.5] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting data from url: http://192.168.96.5:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + +* ***Normal nodes*** (including Aggregator node), for logs about collecting metrics from their Local Netdata agents. E.g. + + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + + + +------ + +## B) Support for Monitoring Self-Healing +> Feature Quick Notes: +> - Self-Healing refers to recovering the monitoring software running at the nodes. +> - In Normal nodes, specifically refers to recovering of EMS client and/or Netdata agent. +> - In RL nodes, refers to recovering Netdata agent only. + + + +#### Design Choices + +1. Each EMS client (in a Normal node) is responsible for recovering the Local Netdata agent, collocated with it. +2. When clustering is used (i.e. in a 3-level topology), Aggregator is responsible for recovering other nodes in its cluster, both Normal and RL. +3. When clustering is not used (i.e. in a 2-level topology), EMS server is responsible for recovering nodes (both Normal and RL). + + + +#### Self-Healing actions + +We distinguish between monitoring topologies: + +* **2-LEVEL Monitoring topology:** Only EMS server and nodes (Normal & RL) are used. No Aggregators or clustering. + + * EMS server will try to recover any *Normal node* that disconnects and not reconnects after a configured period of time. + + ***Condition:*** + + * EMS client disconnects and not re-connects after X seconds + + ***Recovery steps taken by EMS server:*** + + * SSH to node (assuming it is a VM) + * Kill EMS client (if it is still running) + * Launch EMS client + * Close SSH connection + * Wait for a configured period of time for recovered EMS client to reconnect to EMS server + * After that period of time, the process is repeated (up to a configured number of retries, and then gives up). + + * EMS server will try to recovery any *RL node* with inaccessible Netdata agent. + + ***Condition:*** + + * X consecutive connection failures to Netdata agent occur. + + ***Recovery steps taken by EMS server:*** + + * SSH to node (assuming it is a VM) + * Kill Netdata (if it is still running) + * Launch Netdata + * Close SSH connection + * Reset the consecutive failures counter. + + +* **3-LEVEL Monitoring topology:** EMS server, Aggregators (one per cluster), and Nodes in clusters exist. Use of clustering. + + * Aggregator will try to recover any *Normal node* that leaves the cluster and not joins back in a configured period of time. + + ***Condition:*** + + * EMS client leaves cluster and not joins back after X seconds + + ***Recovery steps taken by Aggregators:*** + + * Contact EMS server to get node's credentials + * SSH to node (assuming it is a VM) + * Kill EMS client (if it is still running) + * Launch EMS client + * Close SSH connection + * Wait for a configured period of time for EMS client to join back to cluster + * After that period of time the process is repeated (up to a configured number of retries, and then it gives up and notifies EMS server) + * When EMS client joins to cluster or in case of giving up, the node credentials are cleared from Aggregator's cache. + + * Aggregator will try to recover any *RL node* with inaccessible Netdata agent. + + ***Condition:*** + + * X consecutive connection failures to Netdata agent occur. + + ***Recovery steps taken by Aggregators:*** + + * Contact EMS server to get node's credentials + * SSH to node (assuming it is a VM) + * Kill Netdata agent (if it is still running) + * Launch Netdata agent + * Close SSH connection + * Reset the consecutive failures counter + * On successful connection to Netdata agent the node credentials are cleared from Aggregator cache. + + +* **2-LEVEL or 3-LEVEL Monitoring topology** + + * Any Normal node will try to recover its Local Netdata agent, if it becomes inaccessible. + + ***Condition:*** + + * X consecutive connection failures to Local Netdata agent occur. + + ***Recovery steps (taken by NORMAL node):*** + + * Kill Netdata agent (if it is still running) + * Launch Netdata agent + * Reset the consecutive failures counter + + + +### Test Cases for 2-LEVEL topology + +> ***PREREQUISITE:*** +> +> You need a CAMEL model with a 2-LEVEL monitoring topology: +> +> * with two Requirement Sets: +> - for Normal nodes: 4 cores, 4GB RAM, >1 GB Disk, and +> - for RL nodes: 1-2 cores, or <2GB RAM, or <1GB Disk +> * with 1-2 components with Requirement Set #1 (Normal nodes) +> * with 1-2 components with Requirement Set #2 (RL nodes) +> * with no Groupings used in Metric Model. +> +> This CAMEL model is ***common*** to the following test cases, unless another CAMEL model is specified. +> +> CAMEL model MUST be re-deployed after each test case execution. + + + +**B.1.a) Successful recovery of an EMS client in a Normal node** + +> Test Case Quick Notes: +> - Kill EMS client of any Normal node. +> - The EMS server will recover the killed EMS client after a configured period of time. +> - Check EMS server logs for disconnection, recovery actions and re-connection messages. + +**After Application deployment...** + + * Connect to a Normal node and ***kill*** EMS client + +**Next, check the logs of:** + + * ***EMS server***, for messages reporting an EMS client disconnection, the recovery attempt(s) and EMS client re-connection. + + *

EMS server log: An EMS client disconnected

* + ``` + e.m.e.b.server.ClientShellCommand : #00000==> Signaling client to exit + e.m.e.b.server.ClientShellCommand : #00000--> Thread stops + e.m.e.b.s.coordinator.NoopCoordinator : TwoLevelCoordinator: unregister(): Method invoked. CSC: ClientShellCommand_#00000 + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: -------------------------------------------------- + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: Client unregistered: #00000 @ 172.29.0.3 + e.m.e.b.c.s.ClientRecoveryPlugin : ClientRecoveryPlugin: processExitEvent(): client-id=#00000, client-address=172.29.0.3 + ``` + *

EMS server log: EMS client recovery actions

* + ``` + e.m.e.b.c.s.ClientRecoveryPlugin : ClientRecoveryPlugin: runClientRecovery(): Starting client recovery: node-info=NodeRegistryEntry(ipAddress=172.29.0.3, clientId=VM-UBUNTU-vm1-vm1-AWS-vm1-85499eeb-14bc-481d-9c42-eac879845450, baguetteServer=eu.melodi + o.a.s.c.k.AcceptAllServerKeyVerifier : Server at /172.29.0.3:22 presented unverified EC key: SHA256:gNU4ScwysUpv050SaorPj7zlZrkiyGq4YSsOGBl+DCk + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: Session will be recorded in file: /logs/172.29.0.3-22-2022.02.16.09.33.31.121-0.txt + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Connected to remote host: task #0: host: 172.29.0.3:22 + e.m.e.b.c.install.SshClientInstaller : + ---------------------------------------------------------------------- + Task #0 : Instruction Set: Restarting Baguette agent at VM node + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: Executing installation instructions set: Restarting Baguette agent at VM node + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: Executing instruction 1/2: Killing previous EMS client process + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: EXEC: /opt/baguette-client/bin/kill.sh + o.a.s.c.session.ClientConnectionService : globalRequest(ClientConnectionService[ClientSessionImpl[ubuntu@/172.29.0.3:22]])[hostkeys-00@openssh.com, want-reply=false] failed (SshException) to process: EdDSA provider not supported + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: EXEC: exit-status=0 + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: Executing instruction 2/2: Starting new EMS client process + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: EXEC: /opt/baguette-client/bin/run.sh + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: EXEC: exit-status=0 + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task #0: Installation Instructions set succeeded: Restarting Baguette agent at VM node + e.m.e.b.c.install.SshClientInstaller : + ------------------------------------------------------------------------- + Task #0 : Instruction sets processed: successful=1, failed=0, exit-result=SUCCESS + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Disconnected from remote host: task #0: host: 172.29.0.3:22 + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Task completed successfully #0 + e.m.e.b.c.s.ClientRecoveryPlugin : ClientRecoveryPlugin: runClientRecovery(): Client recovery completed: result=true, node-info=NodeRegistryEntry(ipAddress=172.29.0.3, clientId=VM-UBUNTU-vm1-vm1-AWS-vm1-85499eeb-14bc-481d-9c42-eac879845450, baguetteSe + ``` + *

EMS server log: EMS client reconnected

* + ``` + o.a.s.s.session.ServerUserAuthService : Session user-bbb5b809-3296-485c-a605-cc8bae646bbb@/172.29.0.3:39696 authenticated + e.m.e.b.server.ClientShellCommand : #00001--> Got session : ServerSessionImpl[user-bbb5b809-3296-485c-a605-cc8bae646bbb@/172.29.0.3:39696] + e.m.e.b.server.ClientShellCommand : #00001==> Thread started + e.m.e.b.server.ClientShellCommand : #00001--> Client Id: VM-UBUNTU-vm1-vm1-AWS-vm1-85499eeb-14bc-481d-9c42-eac879845450 + e.m.e.b.server.ClientShellCommand : #00001--> Broker URL: ssl://172.29.0.3:61617?daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true + e.m.e.b.server.ClientShellCommand : #00001--> Broker Username: user-local-Q1mnKfNgzM + e.m.e.b.server.ClientShellCommand : #00001--> Broker Password: xityAHGDhIiVeAxJdfax + e.m.e.b.server.ClientShellCommand : #00001--> Broker Cert.: -----BEGIN CERTIFICATE----- + ......................... + -----END CERTIFICATE----- + e.m.e.b.server.ClientShellCommand : #00001--> Adding/Replacing client certificate in Truststore: alias=172.29.0.3 + e.m.e.b.server.ClientShellCommand : #00001--> Added/Replaced client certificate in Truststore: alias=172.29.0.3, CN=C=GR, ST=Attika, L=Athens, O=Institute of Communication and Computer Systems (ICCS), OU=Information Management Unit (IMU), CN=172.29.0.3, certificate-na + e.m.e.b.s.coordinator.NoopCoordinator : TwoLevelCoordinator: register(): Method invoked. CSC: ClientShellCommand_#00001 + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: -------------------------------------------------- + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: Sending grouping configurations to client #00001... + ......................... + e.m.e.b.server.ClientShellCommand : sendGroupingConfiguration: Serialization of Grouping configuration for PER_INSTANCE: rO0ABXNyACt......................... + e.m.e.b.server.ClientShellCommand : #00001==> PUSH : SET-GROUPING-CONFIG rO0ABXNyACt......................... + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: Sending grouping configurations to client #00001... done + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: -------------------------------------------------- + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: Setting active grouping of client #00001: PER_INSTANCE + e.m.e.b.server.ClientShellCommand : #00001==> PUSH : SET-ACTIVE-GROUPING PER_INSTANCE + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: -------------------------------------------------- + e.m.e.b.server.ClientShellCommand : #00001--> Client grouping changed: null --> PER_INSTANCE + ``` + * ***Normal node where EMS client killed***, for EMS client's logs indicating its restart. + *

Normal node: EMS client restarts

* + ``` + Starting baguette client... + EMS_CONFIG_DIR=/opt/baguette-client/conf + LOG_FILE=/opt/baguette-client/logs/output.txt + ____ _ _ _____ _ _ _ + | _ \ | | | | / ____| (_) | | + | |_) | __ _ __ _ _ _ ___| |_| |_ ___ | | | |_ ___ _ __ | |_ + | _ < / _` |/ _` | | | |/ _ \ __| __/ _ \ | | | | |/ _ \ '_ \| __| + | |_) | (_| | (_| | |_| | __/ |_| || __/ | |____| | | __/ | | | |_ + |____/ \__,_|\__, |\__,_|\___|\__|\__\___| \_____|_|_|\___|_| |_|\__| + __/ | + |___/ + Starting BaguetteClient v4.5.0-SNAPSHOT on 21845bcaf772 with PID 779 (/opt/baguette-client/jars/baguette-client-4.5.0-SNAPSHOT.jar started by ubuntu in /opt/baguette-client) + No active profile set, falling back to default profiles: default + loadCachedClientId: Used cached Client Id: null + Password encoder class name is empty. Default instance of PasswordEncoder will be created + ......................... + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ......................... + ``` + * ***Other Normal nodes***, for NO logs indicating failure or recovery attempts. + + + +**B.1.b) Failed recovery of EMS client in a Normal node** + +> Test Case Quick Notes: +> - Kill the VM of any Normal node. +> - The EMS server will try to connect to the affected VM but fail. +> - After a configured number of retries EMS server will give up. + +**After Application deployment...** + + * Terminate the VM of a Normal node + +**Next, check the logs of:** + + * ***EMS server***, for messages reporting an EMS client disconnection, failed recovery attempts and giving up recovery + + *

EMS server log: An EMS client disconnected

* + ``` + e.m.e.b.server.ClientShellCommand : #00001==> Signaling client to exit + e.m.e.b.server.ClientShellCommand : #00001--> Thread stops + e.m.e.b.s.coordinator.NoopCoordinator : TwoLevelCoordinator: unregister(): Method invoked. CSC: ClientShellCommand_#00001 + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: -------------------------------------------------- + e.m.e.b.s.c.TwoLevelCoordinator : TwoLevelCoordinator: Client unregistered: #00001 @ 172.29.0.3 + e.m.e.b.c.s.ClientRecoveryPlugin : ClientRecoveryPlugin: processExitEvent(): client-id=#00001, client-address=172.29.0.3 + ``` + *

EMS server log: EMS client recovery actions and give up message

* + ``` + e.m.e.b.c.s.ClientRecoveryPlugin : ClientRecoveryPlugin: runClientRecovery(): Starting client recovery: node-info=NodeRegistryEntry(ipAddress=172.29.0.3, clientId=VM-UBUNTU-vm1-vm1-AWS-vm1-85499eeb-14bc-481d-9c42-eac879845450, baguetteServer=eu.melodi + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Error while connecting to remote host: task #0: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Failed executing task #0, Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + ......................... + ......................... + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Retry 5/5 executing task #0 + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Error while connecting to remote host: task #0: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Failed executing task #0, Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + + e.m.e.b.c.install.SshClientInstaller : SshClientInstaller: Giving up executing task #0 after 5 retries + e.m.e.b.c.s.ClientRecoveryPlugin : ClientRecoveryPlugin: runClientRecovery(): Client recovery completed: result=false, node-info=NodeRegistryEntry(ipAddress=172.29.0.3, clientId=VM-UBUNTU-vm1-vm1-AWS-vm1-85499eeb-14bc-481d-9c42-eac879845450, baguetteS + ``` + * ***Normal nodes that operate***, for NO logs indicating any failure or recovery attempts + + + +**B.2.a) Successful recovery of a Netdata agent in a RL node** + +> Test Case Quick Notes: +> - Kill Netdata agent of any RL node. +> - The EMS server will recover the killed Netdata agent after a configured period of time. +> - Check EMS server log messages reporting failures to collect metrics, recovery actions, and successful metrics collection. + +**After Application deployment...** + + * Connect to a RL node and kill Netdata agent. + + *

EMS server log: Failed metric collection attempts from a Netdata agent

* + ``` + ......................... Not yet implemented + ``` + +**Next, check the logs of:** + + * ***EMS server***, for logs reporting connection failure to a Netdata agent, and recovery actions. + + *

EMS server log: Netdata agent recovery actions

* + ``` + ......................... Not yet implemented + ``` + * ***RL node with killed Netdata***, check if the Netdata processes have started again. + *

RL node shell: Recovered Netdata agent process

* + ``` + ......................... Not yet implemented + ``` + * ***Normal nodes (that operate)***, for NO Logs indicating failure or recovery attempts. + + + +**B.2.b) Failed recovery of a Netdata agent in a RL node** + +> Test Case Quick Notes: +> - Kill the VM of any RL node. +> - The EMS server will try to connect to the affected VM but fail. +> - After a configured number of retries EMS server will give up. + +**After Application deployment...** + + * Terminate the VM of a RL node + +**You need to check the logs of:** + + * ***EMS server***, for logs reporting connection failure to a Netdata agent, and then a number of failed attempts to connect to VM. + + *

EMS server log: Failed metric collection attempts from a Netdata agent

* + ``` + ......................... Not yet implemented + ``` + *

EMS server log: Failed Netdata agent recovery actions and give up message

* + ``` + ......................... Not yet implemented + ``` + * ***Normal nodes (that operate)***, for NO logs indicating connection failures or recovery actions. + + + +**B.3) Successful recovery of a Netdata agent in a Normal node** + +> Test Case Quick Notes: +> - Kill Netdata agent of any Normal node. +> - The EMS client of the node will recover the killed Netdata agent after a configured period of time. +> - Check EMS client's logs for messages reporting failures to collect metrics, recovery actions, and successful metrics collection. + +**After Application deployment...** + + * Connect to a Normal node and kill Netdata agent. + +**Next, check the logs of:** + + * ***EMS server***, for No log messages indicating connection failures to Netdata, or recovery actions. + * ***Normal node with killed Netdata***, check if the Netdata processes have started again. Also check EMS client's log messages reporting failed metric collections, recovery actions, and successful metric collection. + + *

Normal node - EMS client log: Failed attempts to collect metrics from Local Netdata agent

* + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: , #errors=1, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://127.0.0.1:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: , #errors=2, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://127.0.0.1:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: , #errors=3, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://127.0.0.1:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + Collectors::Netdata: Too many consecutive errors occurred while attempting to collect metrics from node: , num-of-errors=3 + Collectors::Netdata: Will pause metrics collection from node for 60 seconds: + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=null, address= + ``` + *

Normal node - EMS client log: Local Netdata agent recovery actions

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=null, address= + ShellRecoveryTask: runNodeRecovery(): Executing 3 recovery commands + ############## Initial wait...... + ############## Waiting for 5000ms after Initial wait...... + ############## Sending Netdata agent kill command...... + ############## Waiting for 2000ms after Sending Netdata agent kill command...... + ############## Sending Netdata agent start command...... + ############## Waiting for 10000ms after Sending Netdata agent start command...... + ShellRecoveryTask: runNodeRecovery(): Executed 3 recovery commands + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + OUT> /opt/baguette-client + ERR> -U: 1: -U: Syntax error: Unterminated quoted string + ERR> 2022-02-16 10:23:29: netdata INFO : MAIN : CONFIG: cannot load cloud config '/var/lib/netdata/cloud.d/cloud.conf'. Running with internal defaults. + ``` + *

Normal node - EMS client log: Successful metrics collection from Local Netdata agent

* + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + + Collectors::Netdata: Resumed metrics collection from node: + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=null, address= + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + * ***Normal nodes (that operate)***, for NO logs indicating connection failures or recovery actions. + + + +### Test Cases for 3-LEVEL topology + +> ***PREREQUISITE:*** +> +> You need a CAMEL model for 3-LEVEL topology: +> +> * with two Requirement Sets: +> - for Normal nodes: 4 cores, 4GB RAM, >1 GB Disk, and +> - for RL nodes: 1-2 cores, or <2GB RAM, or <1GB Disk, +> * with 1-2 COMPONENTS with Requirement Set #1 (Normal nodes) +> * with 1-2 COMPONENTS with Requirement Set #2 (RL nodes) +> * with three (3) Groupings used in the Metric Model (`GLOBAL`, `PER_ZONE`, `PER_INSTANCE`). +> +> This CAMEL model is ***common*** to the following test cases, unless another CAMEL model is specified. +> +> CAMEL model MUST be re-deployed after each test case execution. + + + +**B.4.a) Successful recovery of an EMS client in a clustered Normal node** + +> Test Case Quick Notes: +> - Kill EMS client of any Normal node except the Aggregator. +> - The Aggregator will recover the killed EMS client after a configured period of time. +> - Check Aggregator log messages for node leaving cluster, recovery actions, and node joining back. + +**After Application deployment...** + + * Connect to a Normal node, except Aggregator, and ***kill*** EMS client + +**Next, check the logs of:** + + * ***EMS server***, for Aggregator's query for node credentials. + *

EMS server log: Aggregator queries for node's credentials

* + ``` + e.m.e.b.server.ClientShellCommand : #00000==> PUSH : {"random":"cecab3d4-4c09-43b1-b6fa-3534d37bbc8f","zone-id":"IMU-ZONE","address":"192.168.16.4","provider":"AWS","name":"vm2","ssh.port":"22","ssh.username":"ubuntu","ssh.password":"ubuntu","id":"vm2","type":"VM","operatingSystem":"UBUNTU","CLIENT_ID":"VM-UBUNTU-vm2-vm2-AWS-vm2-cecab3d4-4c09-43b1-b6fa-3534d37bbc8f",......................... + ``` + Note: EMS client disconnection from EMS server will also be logged in EMS server logs, but no recovery action will be taken by EMS server. + + * ***Aggregator***, for log messages about, (i) EMS client leaving cluster, (ii) recovery actions, and (iii) EMS client joining back to the cluster. + *

Aggregator log: An EMS client left cluster

* + ``` + CLM: MEMBER_REMOVED: node=node_3866738cb0f4_2002 + BRU: Brokers after cluster change: [Member{id=node_581d745be52c_2001, address=192.168.16.3:2001, properties={aggregator-connection-configuration=eyJncm91cGluZyI6I......................... + SEND: SERVER-GET-NODE-SSH-CREDENTIALS 192.168.16.4 + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=node_3866738cb0f4_2002, address=192.168.16.4 + ``` + *

Aggregator log: EMS client recovery actions

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=node_3866738cb0f4_2002, address=192.168.16.4 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.16.4, port=22, username=ubuntu + Connecting to server... + SSH client is ready + VmNodeRecoveryTask: runNodeRecovery(): Executing 3 recovery commands + ############## Initial wait...... + ############## Waiting for 5000ms after Initial wait...... + ############## Sending baguette client kill command...... + ############## Waiting for 2000ms after Sending baguette client kill command...... + ############## Sending baguette client start command...... + ############## Waiting for 10000ms after Sending baguette client start command...... + SET-CLIENT-CONFIG rO0ABXNyAClldS5tZWxvZGljLmV2ZW50LnV0aWwuQ2xpZW50Q29uZmlndXJhdGlvbiAe4raCjfZzAgABTAASbm9kZXNXaXRob3V0Q2xpZW50dAAPTGphdmEvdXRpbC9TZXQ7eHBzcgARamF2YS51dGlsLkhhc2hTZXS6RIWVlri3NAMAAHhwdwwAAAAQP0AAAAAAAAB4 + New client config.: ClientConfiguration(nodesWithoutClient=[]) + VmNodeRecoveryTask: runNodeRecovery(): Executed 3 recovery commands + VmNodeRecoveryTask: disconnectFromNode(): Disconnecting from node: address=192.168.16.4, port=22, username=ubuntu + Stopping SSH client... + SSH client stopped + OUT> Last login: Sat Feb 12 10:40:09 2022 from 172.29.0.4 + OUT> + OUT> pwd + OUT> ubuntu@3866738cb0f4:~$ pwd + OUT> /home/ubuntu + OUT> ubuntu@3866738cb0f4:~$ /opt/baguette-client/bin/kill.sh + OUT> Baguette client is not running + OUT> ubuntu@3866738cb0f4:~$ /opt/baguette-client/bin/run.sh + OUT> Starting baguette client... + OUT> EMS_CONFIG_DIR=/opt/baguette-client/conf + OUT> LOG_FILE=/opt/baguette-client/logs/output.txt + OUT> Baguette client PID: 973 + VmNodeRecoveryTask: redirectSshOutput(): Connection closed: id=OUT + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + *

Aggregator log: EMS client joined back to cluster

* + ``` + CLM: MEMBER_ADDED: node=node_3866738cb0f4_2002 + BRU: Brokers after cluster change: [Member{id=node_581d745be52c_2001, address=192.168.16.3:2001, properties={aggregator-connection-configuration=eyJncm91cGluZyI6I......................... + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=node_3866738cb0f4_2002, address=192.168.16.4 + ``` + * ***Normal node whose EMS client killed***, for EMS client's logs indicating its restart. + *

Normal node: EMS client restarts

* + ``` + Starting baguette client... + EMS_CONFIG_DIR=/opt/baguette-client/conf + LOG_FILE=/opt/baguette-client/logs/output.txt + ____ _ _ _____ _ _ _ + | _ \ | | | | / ____| (_) | | + | |_) | __ _ __ _ _ _ ___| |_| |_ ___ | | | |_ ___ _ __ | |_ + | _ < / _` |/ _` | | | |/ _ \ __| __/ _ \ | | | | |/ _ \ '_ \| __| + | |_) | (_| | (_| | |_| | __/ |_| || __/ | |____| | | __/ | | | |_ + |____/ \__,_|\__, |\__,_|\___|\__|\__\___| \_____|_|_|\___|_| |_|\__| + __/ | + |___/ + Starting BaguetteClient v4.5.0-SNAPSHOT on 3866738cb0f4 with PID 973 (/opt/baguette-client/jars/baguette-client-4.5.0-SNAPSHOT.jar started by ubuntu in /opt/baguette-client) + No active profile set, falling back to default profiles: default + loadCachedClientId: Used cached Client Id: null + Password encoder class name is empty. Default instance of PasswordEncoder will be created + PasswordUtil.setPasswordEncoder(): PasswordEncoder set to: password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder + PasswordUtil: Initialized default Password Encoder: password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder + BrokerConfig.initializeKeyAndCert(): Initializing keystore, truststore and certificate for Broker-SSL... + KeystoreUtil.initializeKeystoresAndCertificate(): Initializing keystores and certificate + BrokerConfig.initializeKeyAndCert(): Initializing keystore, truststore and certificate for Broker-SSL... done + BrokerConfig: Creating new Broker Service instance: url=ssl://0.0.0.0:61617 + ......................... + ......................... + CLUSTER-JOIN IMU-ZONE GLOBAL:PER_ZONE:PER_INSTANCE start-election=true 192.168.16.4:2002 192.168.16.3:2001 + CLUSTER-JOIN ARGS: cluster-id=IMU-ZONE, groupings=GLOBAL:PER_ZONE:PER_INSTANCE, local-node=192.168.16.4:2002, other-nodes=[192.168.16.3:2001] + CLUSTER-JOIN ARGS: Groupings: global=GLOBAL, aggregator=PER_ZONE, node=PER_INSTANCE + CLM: Local address used for building Atomix: 192.168.16.4:2002 + CLM: Building Atomix: Other members: [Node{id=node_3866738cb0f4_2001, address=192.168.16.3:2001}] + ......................... + ......................... + CLUSTER-EXEC broker list + Cluster executes command: broker list + CLI: Node status and scores: + CLI: node_581d745be52c_2001 [AGGREGATOR, 0.6640625, 9e790362-704c-4d9e-aa74-77f76e297816] + CLI: node_3866738cb0f4_2002 [CANDIDATE, 0.6640625, 44a5afb7-890a-4090-9f80-c65f046aeddd] + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + * ***Other Normal nodes***, for logs about, (i) EMS client leaving cluster, (ii) EMS client joining to cluster, but NO logs about recovery actions. + + + +**B.4.b) Failed recovery of an EMS client in a clustered Normal node** + +> Test Case Quick Notes: +> - Kill the VM of any Normal node, except Aggregator. +> - The Aggregator will try to connect to the affected VM but fail. +> - After a configured number of retries Aggregator will give up. + +**After Application deployment...** + + * Terminate the VM of a Normal node, except the Aggregator's + +**Next, check the logs of:** + + * ***EMS server***, for a recovery Give up message from Aggregator + *

EMS server log: Aggregator queries for node's credentials

* + ``` + e.m.e.b.server.ClientShellCommand : #00000==> PUSH : {"random":"cecab3d4-4c09-43b1-b6fa-3534d37bbc8f","zone-id":"IMU-ZONE","address":"192.168.16.4","provider":"AWS","name":"vm2","ssh.port":"22","ssh.username":"ubuntu","ssh.password":"ubuntu","id":"vm2","type":"VM","operatingSystem":"UBUNTU","CLIENT_ID":"VM-UBUNTU-vm2-vm2-AWS-vm2-cecab3d4-4c09-43b1-b6fa-3534d37bbc8f",......................... + ``` + *

EMS server log: Aggregator give up message

* + ``` + e.m.e.b.server.ClientShellCommand : #00000--> Client notification: CMD=RECOVERY, ARGS=GIVE_UP node_3866738cb0f4_2002 @ 192.168.16.4 + e.m.e.b.server.ClientShellCommand : #00000--> Client Recovery Notification: GIVE_UP: node_3866738cb0f4_2002 @ 192.168.16.4 + ``` + Note: EMS client disconnection from EMS server will also be logged in EMS server logs, but no recovery action will be taken by EMS server. + + * ***Aggregator***, for messages reporting, (i) an EMS client left cluster, (ii) a number of failed connection attempts to the VM, and (iii) a recovery give up message. + *

Aggregator log: An EMS client left cluster

* + ``` + CLM: MEMBER_REMOVED: node=node_3866738cb0f4_2002 + BRU: Brokers after cluster change: [Member{id=node_581d745be52c_2001, address=192.168.16.3:2001, properties={aggregator-connection-configuration=eyJncm91cGluZyI6I......................... + SEND: SERVER-GET-NODE-SSH-CREDENTIALS 192.168.16.4 + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=node_3866738cb0f4_2002, address=192.168.16.4 + ``` + *

Aggregator log: EMS client recovery actions and give up message

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=node_3866738cb0f4_2002, address=192.168.16.4 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.16.4, port=22, username=ubuntu + Connecting to server... + SelfHealingPlugin: EXCEPTION while recovering node: node-address=192.168.16.4 -- Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + ......................... + ......................... + SelfHealingPlugin: Retry #3: Recovering node: id=node_3866738cb0f4_2002, address=192.168.16.4 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.16.4, port=22, username=ubuntu + Connecting to server... + SelfHealingPlugin: EXCEPTION while recovering node: node-address=192.168.16.4 -- Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + ``` + ``` + SelfHealingPlugin: Max retries reached. No more recovery retries for node: id=node_3866738cb0f4_2002, address=192.168.16.4 + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=node_3866738cb0f4_2002, address=192.168.16.4 + NOTIFY-X: RECOVERY GIVE_UP node_3866738cb0f4_2002 @ 192.168.16.4 + ``` + * ***Normal nodes that operate***, for logs about EMS client leaving cluster, and NO logs about recovery actions or EMS client joining back. + + + +**B.5.a) Successful recovery of EMS client of the cluster Aggregator** + +> Test Case Quick Notes: +> - Kill EMS client of the Aggregator. +> - The cluster nodes will elect a new Aggregator. Check logs of any cluster node. +> - The new Aggregator will recover the killed EMS client after a configured period of time. +> - Check new Aggregator log messages for node leaving cluster, being elected as Aggregator, recovery actions, and node joining back. +> - Old Aggregator will join back as a Normal node. + +**After Application deployment...** + + * Connect to the Aggregator node, and ***kill*** EMS client. + +**Next, check the logs of:** + + * ***EMS server***, for message about Aggregator change. + *

EMS server log: A new Aggregator initialized

* + ``` + e.m.e.b.server.ClientShellCommand : #00003--> Client status changed: CANDIDATE --> INITIALIZING + e.m.e.b.server.ClientShellCommand : #00003--> Client grouping changed: PER_INSTANCE --> PER_ZONE + e.m.e.b.s.c.c.ClusteringCoordinator : Updated aggregator of zone: IMU-ZONE -- New aggregator: #00003 @ 192.168.16.4 (VM-UBUNTU-vm2-vm2-AWS-vm2-cecab3d4-4c09-43b1-b6fa-3534d37bbc8f) + e.m.e.b.server.ClientShellCommand : #00003--> Client status changed: INITIALIZING --> AGGREGATOR + ``` + *

EMS server log: Aggregator queries for node's credentials

* + ``` + e.m.e.b.server.ClientShellCommand : #00003==> PUSH : {"random":"8a20f11c-eaf2-4b6e-b827-d8a25a57cb0a","zone-id":"IMU-ZONE","address":"192.168.16.3","provider":"AWS",......................... + ``` + Note: Aggregator disconnection from EMS server will also be logged in EMS server logs, but no recovery action will be taken by EMS server. + + * ***New Aggregator***, for log messages about, (i) EMS client leaving cluster, (ii) being elected as Aggregator, (iii) recovery actions, and (iv) EMS client joining to cluster. + *

New Aggregator log: Old Aggregator left cluster - New Aggregator election

* + ``` + CLM: MEMBER_REMOVED: node=node_581d745be52c_2001 + BRU: Brokers after cluster change: [] + + BRU: Broker election requested: broadcasting election message... + BRU: **** Broker message received: election + BRU: **** BROKER: Starting Broker election: + BRU: Member-Score: node_3866738cb0f4_2002 => 0.6640625 d4f2eb55-c355-4715-8a27-9f7c12c32924 + BRU: Broker: node_3866738cb0f4_2002 + ``` + *

New Aggregator log: Initializing to become the new Aggregator

* + ``` + BRU: Node will become Broker. Initializing... + NOTIFY-STATUS-CHANGE: INITIALIZING + initialize(): Node starts initializing as Aggregator... + ......................... + ......................... + Notifying Baguette Server i am the new aggregator + ......................... + ......................... + BRU: Node is ready to act as Aggregator. Ready + BRU: **** Broker message received: ready node_3866738cb0f4_2002 New config: eyJncm91cGluZyI6IlBFUl9aT05FIiwidXJsIjoic3NsOi8vMTkyLjE2OC4xNi40OjYxNjE3P2RhZW1vbj10cn......................... + BRU: **** BROKER: New Broker is ready: node_3866738cb0f4_2002, New config: eyJncm91cGluZyI6IlBFUl9aT05FIiwidXJsIjoic3NsOi8vMTkyLjE2OC4xNi40OjYxNjE3P2RhZW1vbj10cn......................... + BRU: Node configuration updated: eyJncm91cGluZyI6IlBFUl9aT05FIiwidXJsIjoic3NsOi8vMTkyLjE2OC4xNi40OjYxNjE3P2RhZW1vbj10cn......................... + ``` + *

New Aggregator log: Requesting old Aggregator node's credentials

* + ``` + SEND: SERVER-GET-NODE-SSH-CREDENTIALS 192.168.16.3 + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=node_581d745be52c_2001, address=192.168.16.3 + ``` + *

New Aggregator log: Recovery actions of old Aggregator

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=node_581d745be52c_2001, address=192.168.16.3 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.16.3, port=22, username=ubuntu + Connecting to server... + SSH client is ready + VmNodeRecoveryTask: runNodeRecovery(): Executing 3 recovery commands + ############## Initial wait...... + ############## Waiting for 5000ms after Initial wait...... + ############## Sending baguette client kill command...... + ############## Waiting for 2000ms after Sending baguette client kill command...... + ############## Sending baguette client start command...... + ############## Waiting for 10000ms after Sending baguette client start command...... + SET-CLIENT-CONFIG rO0ABXNyAClldS5tZWxvZGljLmV2ZW50LnV0aWwuQ2xpZW50Q29uZmlndXJhdGlvbiAe4raCjfZzAgABTAASbm9kZXNXaXRob3V0Q2xpZW50dAAPTGphdmEvdXRpbC9TZXQ7eHBzcgARamF2YS51dGlsLkhhc2hTZXS6RIWVlri3NAMAAHhwdwwAAAAQP0AAAAAAAAB4 + New client config.: ClientConfiguration(nodesWithoutClient=[]) + VmNodeRecoveryTask: runNodeRecovery(): Executed 3 recovery commands + VmNodeRecoveryTask: disconnectFromNode(): Disconnecting from node: address=192.168.16.3, port=22, username=ubuntu + Stopping SSH client... + SSH client stopped + OUT> Last login: Sat Feb 12 10:40:09 2022 from 172.29.0.4 + OUT> + OUT> pwd + OUT> ubuntu@581d745be52c:~$ pwd + OUT> /home/ubuntu + OUT> ubuntu@581d745be52c:~$ /opt/baguette-client/bin/kill.sh + OUT> Baguette client is not running + OUT> ubuntu@581d745be52c:~$ /opt/baguette-client/bin/run.sh + OUT> Starting baguette client... + OUT> EMS_CONFIG_DIR=/opt/baguette-client/conf + OUT> LOG_FILE=/opt/baguette-client/logs/output.txt + OUT> Baguette client PID: 1242 + VmNodeRecoveryTask: redirectSshOutput(): Connection closed: id=OUT + ``` + *

New Aggregator log: Old Aggregator joins back to cluster as plain node

* + ``` + CLM: MEMBER_ADDED: node=node_581d745be52c_2001 + BRU: Brokers after cluster change: [Member{id=node_581d745be52c_2001, address=192.168.16.3:2001, properties={aggregator-connection-configuration=eyJncm91cGluZyI6I......................... + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=node_581d745be52c_2001, address=192.168.16.3 + ``` + * ***Old Aggregator node whose EMS client killed***, for EMS client's logs indicating its restart (as a `PER_INSTANCE` node). + *

Normal node: Old Aggregator restarts as a plain Normal node

* + ``` + Starting baguette client... + EMS_CONFIG_DIR=/opt/baguette-client/conf + LOG_FILE=/opt/baguette-client/logs/output.txt + ____ _ _ _____ _ _ _ + | _ \ | | | | / ____| (_) | | + | |_) | __ _ __ _ _ _ ___| |_| |_ ___ | | | |_ ___ _ __ | |_ + | _ < / _` |/ _` | | | |/ _ \ __| __/ _ \ | | | | |/ _ \ '_ \| __| + | |_) | (_| | (_| | |_| | __/ |_| || __/ | |____| | | __/ | | | |_ + |____/ \__,_|\__, |\__,_|\___|\__|\__\___| \_____|_|_|\___|_| |_|\__| + __/ | + |___/ + Starting BaguetteClient v4.5.0-SNAPSHOT on 581d745be52c with PID 1242 (/opt/baguette-client/jars/baguette-client-4.5.0-SNAPSHOT.jar started by ubuntu in /opt/baguette-client) + No active profile set, falling back to default profiles: default + loadCachedClientId: Used cached Client Id: null + Password encoder class name is empty. Default instance of PasswordEncoder will be created + PasswordUtil.setPasswordEncoder(): PasswordEncoder set to: password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder + PasswordUtil: Initialized default Password Encoder: password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder + BrokerConfig.initializeKeyAndCert(): Initializing keystore, truststore and certificate for Broker-SSL... + KeystoreUtil.initializeKeystoresAndCertificate(): Initializing keystores and certificate + BrokerConfig.initializeKeyAndCert(): Initializing keystore, truststore and certificate for Broker-SSL... done + ......................... + ......................... + CLM: Joining cluster... + NOTIFY-STATUS-CHANGE: CANDIDATE + ......................... + ......................... + Joined to cluster + ......................... + ......................... + CLUSTER-EXEC broker list + Cluster executes command: broker list + CLI: Node status and scores: + CLI: node_3866738cb0f4_2002 [AGGREGATOR, 0.6640625, d4f2eb55-c355-4715-8a27-9f7c12c32924] + CLI: node_581d745be52c_2001 [CANDIDATE, 0.6640625, e974ebcd-e11e-4baa-b3cb-fa34242705ff] + ``` + * ***Other Normal nodes***, for log messages about, (i) EMS client leaving cluster, (ii) Aggregator election, (iii) EMS client joining to cluster, but NO logs about recovery actions. + + + +**B.5.b) Failed recovery of EMS client of the cluster Aggregator** + +> Test Case Quick Notes: +> - Kill the VM of the Aggregator. +> - The cluster nodes will elect a new Aggregator. Check logs of any cluster node. +> - The new Aggregator will try to connect to the affected VM but fail. +> - After a configured number of retries new Aggregator will give up. + +**After Application deployment...** + + * Terminate the VM of the Aggregator + +**Next, check the logs of:** + + * ***EMS server***, for one message about Aggregator change, and one about new Aggregator giving up recovery. + *

EMS server log: A new Aggregator initialized

* + ``` + e.m.e.b.server.ClientShellCommand : #00004--> Client status changed: CANDIDATE --> INITIALIZING + e.m.e.b.server.ClientShellCommand : #00004--> Client grouping changed: PER_INSTANCE --> PER_ZONE + e.m.e.b.s.c.c.ClusteringCoordinator : Updated aggregator of zone: IMU-ZONE -- New aggregator: #00004 @ 192.168.16.3 (VM-UBUNTU-vm1-vm1-AWS-vm1-8a20f11c-eaf2-4b6e-b827-d8a25a57cb0a) + e.m.e.b.server.ClientShellCommand : #00004--> Client status changed: INITIALIZING --> AGGREGATOR + ``` + *

EMS server log: New Aggregator queries for node's credentials

* + ``` + e.m.e.b.server.ClientShellCommand : #00004==> PUSH : {"random":"4abf9ae2-b7fc-4e8c-b6d9-464623d1b05f","zone-id":"IMU-ZONE","address":"192.168.16.4",......................... + ``` + *

EMS server log: New Aggregator give up message

* + ``` + e.m.e.b.server.ClientShellCommand : #00004--> Client notification: CMD=RECOVERY, ARGS=GIVE_UP node_3866738cb0f4_2002 @ 192.168.16.4 + e.m.e.b.server.ClientShellCommand : #00004--> Client Recovery Notification: GIVE_UP: node_3866738cb0f4_2002 @ 192.168.16.4 + ``` + Note: Aggregator disconnection from EMS server will also be logged in EMS server logs, but no recovery action will be taken by EMS server. + + * ***New Aggregator***, for messages reporting, (i) an EMS client left cluster, (ii) being elected as Aggregator, (iii) a number of failed connection attempts to the VM, and (iv) a recovery give up message. + *

New Aggregator log: Old Aggregator left cluster - New Aggregator election

* + ``` + CLM: MEMBER_REMOVED: node=node_3866738cb0f4_2002 + BRU: Brokers after cluster change: [] + BRU: Broker election requested: broadcasting election message... + BRU: **** Broker message received: election + BRU: **** BROKER: Starting Broker election: + BRU: Member-Score: node_581d745be52c_2001 => 0.6640625 e974ebcd-e11e-4baa-b3cb-fa34242705ff + BRU: Broker: node_581d745be52c_2001 + ``` + *

New Aggregator log: Initializing to become the new Aggregator

* + ``` + CLM: MEMBER_REMOVED: node=node_3866738cb0f4_2002 + BRU: Brokers after cluster change: [] + BRU: Broker election requested: broadcasting election message... + BRU: **** Broker message received: election + BRU: **** BROKER: Starting Broker election: + BRU: Member-Score: node_581d745be52c_2001 => 0.6640625 e974ebcd-e11e-4baa-b3cb-fa34242705ff + BRU: Broker: node_581d745be52c_2001 + + BRU: Node will become Broker. Initializing... + 2022-02-16 12:01:34.448 [INFO ] NOTIFY-STATUS-CHANGE: INITIALIZING + initialize(): Node starts initializing as Aggregator... + ......................... + ......................... + Notifying Baguette Server i am the new aggregator + ......................... + ......................... + BRU: Node is ready to act as Aggregator. Ready + BRU: **** Broker message received: ready node_581d745be52c_2001 New config: eyJncm91cGluZyI6IlBFUl9aT05FIiwidXJsIjoic3NsOi8vMTkyLjE2OC4xNi4zOjYxNjE3P2RhZW1vbj10cn......................... + BRU: **** BROKER: New Broker is ready: node_581d745be52c_2001, New config: eyJncm91cGluZyI6IlBFUl9aT05FIiwidXJsIjoic3NsOi8vMTkyLjE2OC4xNi4zOjYxNjE3P2RhZW1vbj10cn......................... + BRU: Node configuration updated: eyJncm91cGluZyI6IlBFUl9aT05FIiwidXJsIjoic3NsOi8vMTkyLjE2OC4xNi4zOjYxNjE3P2RhZW1vbj10cn......................... + ``` + *

New Aggregator log: Requesting old Aggregator node's credentials

* + ``` + SEND: SERVER-GET-NODE-SSH-CREDENTIALS 192.168.16.4 + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=node_3866738cb0f4_2002, address=192.168.16.4 + ``` + *

New Aggregator log: Failing recovery actions of old Aggregator

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=node_3866738cb0f4_2002, address=192.168.16.4 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.16.4, port=22, username=ubuntu + Connecting to server... + SelfHealingPlugin: EXCEPTION while recovering node: node-address=192.168.16.4 -- Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + ......................... + ......................... + SelfHealingPlugin: Retry #3: Recovering node: id=node_3866738cb0f4_2002, address=192.168.16.4 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.16.4, port=22, username=ubuntu + Connecting to server... + SelfHealingPlugin: EXCEPTION while recovering node: node-address=192.168.16.4 -- Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + ``` + *

New Aggregator log: Recovery actions Give Up message

* + ``` + SelfHealingPlugin: Max retries reached. No more recovery retries for node: id=node_3866738cb0f4_2002, address=192.168.16.4 + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=node_3866738cb0f4_2002, address=192.168.16.4 + NOTIFY-X: RECOVERY GIVE_UP node_3866738cb0f4_2002 @ 192.168.16.4 + ``` + * ***Normal nodes that operate***, for log messages about, (i) EMS client leaving cluster, (ii) Aggregator election, but NO logs about recovery actions, or EMS client joining back to cluster. + + + +**B.6.a) Successful recovery of Netdata agent in a clustered RL node** + +> Test Case Quick Notes: +> - Kill Netdata agent of any RL node. +> - The Aggregator will recover the killed Netdata agent after a configured period of time. +> - Check Aggregator log messages reporting failures to collect metrics, recovery actions, and successful metrics collection. + +**After Application deployment...** + + * Connect to a RL node and ***kill*** Netdata agent. + +**Next, check the logs of:** + + * ***EMS server***, for NO logs indicating a Netdata failure and recovery. + *

EMS server log: Aggregator queries for RL node's credentials

* + ``` + e.m.e.b.server.ClientShellCommand : #00000==> PUSH : {"random":"4b676a58-e00e-4ddf-a21e-b1c0d1382cd6","zone-id":"IMU-ZONE","address":"192.168.96.2","provider":"AWS",......................... + ``` + * ***Aggregator***, for logs reporting, (i) connection failures to a Netdata agent, (ii) recovery actions, and (iii) successful connection to Netdata agent and collection of metrics. + *

Aggregator log: Failed metric collection attempts from a RL node's Netdata agent

* + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: 192.168.96.2, #errors=1, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://192.168.96.2:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: 192.168.96.2, #errors=2, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://192.168.96.2:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: 192.168.96.2, #errors=3, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://192.168.96.2:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + Collectors::Netdata: Too many consecutive errors occurred while attempting to collect metrics from node: 192.168.96.2, num-of-errors=3 + Collectors::Netdata: Pausing collection from Node: 192.168.96.2 + ``` + *

Aggregator log: Requesting RL node's credentials

* + ``` + SEND: SERVER-GET-NODE-SSH-CREDENTIALS 192.168.96.2 + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=null, address=192.168.96.2 + ``` + *

Aggregator log: Netdata agent recovery actions

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=null, address=192.168.96.2 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.96.2, port=22, username=ubuntu + Connecting to server... + SSH client is ready + VmNodeRecoveryTask: runNodeRecovery(): Executing 3 recovery commands + ############## Initial wait...... + ############## Waiting for 5000ms after Initial wait...... + ############## Sending Netdata agent kill command...... + ############## Waiting for 2000ms after Sending Netdata agent kill command...... + ############## Sending Netdata agent start command...... + ############## Waiting for 10000ms after Sending Netdata agent start command...... + VmNodeRecoveryTask: runNodeRecovery(): Executed 3 recovery commands + VmNodeRecoveryTask: disconnectFromNode(): Disconnecting from node: address=192.168.96.2, port=22, username=ubuntu + Stopping SSH client... + SSH client stopped + Collectors::Netdata: Resuming collection from Node: 192.168.96.2 + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=null, address=192.168.96.2 + OUT> Last login: Sat Feb 12 10:40:09 2022 from 172.29.0.4 + OUT> + OUT> pwd + OUT> ubuntu@ec17d3e87fb4:~$ pwd + OUT> /home/ubuntu + OUT> ubuntu@ec17d3e87fb4:~$ + OUT> < -U netdata -o "pid" --no-headers | xargs kill -9' + OUT> + OUT> Usage: + OUT> kill [options] [...] + OUT> + OUT> Options: + OUT> [...] send signal to every listed + OUT> -, -s, --signal + OUT> specify the to be sent + OUT> -l, --list=[] list all signal names, or convert one to a name + OUT> -L, --table list all signal names in a nice table + OUT> + OUT> -h, --help display this help and exit + OUT> -V, --version output version information and exit + OUT> + OUT> For more details see kill(1). + OUT> ubuntu@ec17d3e87fb4:~$ sudo netdata + OUT> 2022-02-16 12:27:55: netdata INFO : MAIN : CONFIG: cannot load cloud config '/var/lib/netdata/cloud.d/cloud.conf'. Running with internal defaults. + VmNodeRecoveryTask: redirectSshOutput(): Connection closed: id=OUT + ``` + *

Aggregator log: Successful metrics collection from RL node's Netdata agent

* + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + * ***RL node with killed Netdata***, check if the Netdata processes have started again. + *

RL node shell: Recovered Netdata agent process

* + ```sh + # ps -ef |grep netdata + root 610 29 0 12:27 pts/0 00:00:00 grep --color=auto netd + ......................... + ......................... + # ps -ef |grep netdata + netdata 623 1 5 12:27 ? 00:00:51 netdata + netdata 625 623 0 12:27 ? 00:00:02 /usr/sbin/netdata --special-spawn-server + root 894 623 0 12:28 ? 00:00:05 /usr/libexec/netdata/plugins.d/apps.plugin 1 + netdata 1050 623 0 12:28 ? 00:00:04 /usr/libexec/netdata/plugins.d/go.d.plugin 1 + root 1105 29 0 12:45 pts/0 00:00:00 grep --color=auto netd + ``` + * ***Normal nodes (that operate)***, for NO logs indicating connection failures or recovery action. + + + +**B.6.b) Failed recovery of Netdata agent in a clustered RL node** + +> Test Case Quick Notes: +> - Kill the VM of any RL node. +> - The EMS server will try to connect to the affected VM but fail. +> - After a configured number of retries EMS server will give up. + +**After Application deployment...** + + * Terminate the VM of a RL node + +**You need to check the logs of:** + + * ***EMS server***, for NO logs indicating a Netdata failure and recovery, BUT reporting a recovery give up from Aggregator. + *

EMS server log: Aggregator queries for RL node's credentials

* + ``` + e.m.e.b.server.ClientShellCommand : #00000==> PUSH : {"random":"4b676a58-e00e-4ddf-a21e-b1c0d1382cd6","zone-id":"IMU-ZONE","address":"192.168.96.2","provider":"AWS",......................... + ``` + *

EMS server log: Aggregator give up message

* + ``` + e.m.e.b.server.ClientShellCommand : #00000--> Client notification: CMD=RECOVERY, ARGS=GIVE_UP null @ 192.168.96.2 + e.m.e.b.server.ClientShellCommand : #00000--> Client Recovery Notification: GIVE_UP: null @ 192.168.96.2 + e.m.e.baguette.server.BaguetteServer : BaguetteServer.onMessage: Marked Node as Failed: 192.168.96.2 + ``` + * ***Aggregator***, for logs reporting (i) connection failures to a Netdata agent, (ii) a number of failed attempts to connect to VM, and (iii) a recovery give up message. + *

Aggregator log: Failed metric collection attempts from a RL node's Netdata agent

* + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: 192.168.96.2, #errors=1, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://192.168.96.2:19999/api/v1/allmetrics": connect timed out; nested exception is java.net.SocketTimeoutException: connect timed out -> java.net.SocketTimeoutException: connect timed out + + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: 192.168.96.2, #errors=2, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://192.168.96.2:19999/api/v1/allmetrics": connect timed out; nested exception is java.net.SocketTimeoutException: connect timed out -> java.net.SocketTimeoutException: connect timed out + + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Collectors::Netdata: Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: 192.168.96.2, #errors=3, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://192.168.96.2:19999/api/v1/allmetrics": connect timed out; nested exception is java.net.SocketTimeoutException: connect timed out -> java.net.SocketTimeoutException: connect timed out + Collectors::Netdata: Too many consecutive errors occurred while attempting to collect metrics from node: 192.168.96.2, num-of-errors=3 + Collectors::Netdata: Pausing collection from Node: 192.168.96.2 + ``` + *

Aggregator log: Requesting RL node's credentials

* + ``` + SEND: SERVER-GET-NODE-SSH-CREDENTIALS 192.168.96.2 + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=null, address=192.168.96.2 + ``` + *

Aggregator log: Netdata agent (failing) recovery actions

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=null, address=192.168.96.2 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.96.2, port=22, username=ubuntu + Connecting to server... + SelfHealingPlugin: EXCEPTION while recovering node: node-address=192.168.96.2 -- Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + + Collecting metrics from local node... + Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Metrics: extracted=0, published=0, failed=0 + Collecting metrics from remote nodes (without EMS client): [192.168.96.2] + Node is in ignore list: 192.168.96.2 + ......................... + ......................... + SelfHealingPlugin: Retry #3: Recovering node: id=null, address=192.168.96.2 + VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address=192.168.96.2, port=22, username=ubuntu + Connecting to server... + SelfHealingPlugin: EXCEPTION while recovering node: node-address=192.168.96.2 -- Exception: + java.net.NoRouteToHostException: No route to host + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.checkConnect(Native Method) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishConnect(UnixAsynchronousSocketChannelImpl.java:252) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:198) + at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:213) + at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293) + at java.lang.Thread.run(Thread.java:748) + ``` + *

Aggregator log: Netdata agent recovery Give Up message

* + ``` + SelfHealingPlugin: Max retries reached. No more recovery retries for node: id=null, address=192.168.96.2 + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=null, address=192.168.96.2 + Collectors::Netdata: Giving up collection from Node: 192.168.96.2 + NOTIFY-X: RECOVERY GIVE_UP null @ 192.168.96.2 + ``` + * ***Normal nodes (that operate)***, for NO logs indicating connection failures or recovery actions. + + + +**B.7) Successful recovery of local Netdata agent, in a clustered Normal node (including Aggregator)** + +> Test Case Quick Notes: +> - Kill Netdata agent of any Normal node. +> - The EMS client of the affected node will recover the killed Netdata agent after a configured period of time. +> - Check EMS client's log for messages reporting failures to collect metrics, recovery actions, and successful metrics collection. + +**After Application deployment...** + + * Connect to a Normal node and ***kill*** Netdata agent. + +**Next, check the logs of:** + + * ***EMS server***, for No log messages indicating connection failures to a Netdata agent or recovery actions. + * ***Aggregator***, for No log messages indicating connection failures to a Netdata agent or recovery actions. + * ***Normal node with killed Netdata***, check if the Netdata processes have started again. Also check EMS client's log messages reporting failed metric collection attempts, recovery actions, and successful metric collection. + *

Normal node - EMS client log: Failed attempts to collect metrics from Local Netdata agent

* + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: , #errors=1, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://127.0.0.1:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: , #errors=2, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://127.0.0.1:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Exception while collecting metrics from node: , #errors=3, exception: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://127.0.0.1:19999/api/v1/allmetrics": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused) -> java.net.ConnectException: Connection refused (Connection refused) + Collectors::Netdata: Too many consecutive errors occurred while attempting to collect metrics from node: , num-of-errors=3 + Collectors::Netdata: Will pause metrics collection from node for 60 seconds: + SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id=null, address= + ``` + *

Normal node - EMS client log: Local Netdata agent recovery actions

* + ``` + SelfHealingPlugin: Retry #0: Recovering node: id=null, address= + ShellRecoveryTask: runNodeRecovery(): Executing 3 recovery commands + ############## Initial wait...... + ############## Waiting for 5000ms after Initial wait...... + ############## Sending Netdata agent kill command...... + ############## Waiting for 2000ms after Sending Netdata agent kill command...... + ############## Sending Netdata agent start command...... + ############## Waiting for 10000ms after Sending Netdata agent start command...... + ShellRecoveryTask: runNodeRecovery(): Executed 3 recovery commands + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + OUT> /opt/baguette-client + ERR> -U: 1: -U: Syntax error: Unterminated quoted string + ERR> 2022-02-16 13:21:52: netdata INFO : MAIN : CONFIG: cannot load cloud config '/var/lib/netdata/cloud.d/cloud.conf'. Running with internal defaults. + ``` + *

Normal node - EMS client log: Successful metrics collection from Local Netdata agent

* + ``` + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Node is in ignore list: + + Collectors::Netdata: Resumed metrics collection from node: + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=null, address= + + Collectors::Netdata: Collecting metrics from local node... + Collectors::Netdata: Collecting data from url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + Collectors::Netdata: Metrics: extracted=0, published=0, failed=0 + ``` + * ***Other Normal nodes (that operate)***, for NO logs indicating connection failures or recovery actions. + + + +------ + +## Limitations + +* Clustering is never used for 2-level monitoring topologies. +* When no Normal nodes (and hence no Aggregator) exist in a cluster, no one will collect metrics from the (orphan) RL nodes. +* When no Normal nodes (and hence no Aggregator) exist in a cluster, no one will recover the (orphan) RL nodes. +* If EMS server fails no one will recover it. +* Metric messages are not cached/redirected, if the next node has failed. diff --git a/ems-core/baguette-client-install/pom.xml b/ems-core/baguette-client-install/pom.xml new file mode 100644 index 0000000..66e1895 --- /dev/null +++ b/ems-core/baguette-client-install/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + baguette-client-install + EMS - Baguette Client install utilities + + + + gr.iccs.imu.ems + baguette-server + ${project.version} + compile + + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + provided + + + + + org.rauschig + jarchivelib + 1.2.0 + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + + org.yaml + snakeyaml + + + + diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallationProperties.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallationProperties.java new file mode 100644 index 0000000..277d9cb --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallationProperties.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +@Slf4j +@Data +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "baguette.client.install") +public class ClientInstallationProperties implements InitializingBean { + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("ClientInstallationProperties: {}", this); + } + + enum INSTALLER_TYPE { DEFAULT_INSTALLER, JS_INSTALLER } + + private final Map> osFamilies = new LinkedHashMap<>(); + + private int workers = 1; + private INSTALLER_TYPE installerType = INSTALLER_TYPE.DEFAULT_INSTALLER; + + private String baseDir; // EMS client home directory + private String rootCmd; // Root command (e.g. 'sudo', or 'echo ${NODE_SSH_PASSWORD} | sudo -S ') + private List mkdirs; + private List touchFiles; + private String checkInstalledFile; + + private String downloadUrl; // Base URL of EMS server downloads + @ToString.Exclude + private String apiKey; // API Key for accessing EMS server downloads + private String installScriptUrl; + private String installScriptFile; + + private String archiveSourceDir; // the directory in server that will be archived (it must contain client configuration) + private String archiveDir; // the directory in server where client config. archive will be placed into + private String archiveFile; // name of the client configuration archive (in server) + private String clientConfArchiveFile; // location in VM, where client config. archive will be stored (in BASE64 encoding) + //private String clientConfArchiveDest; // location in VM, where client config. archive will be extracted + + private String serverCertFileAtServer; // location of EMS server certificate in server (in config-files) + private String serverCertFileAtClient; // location in VM, where EMS server certificate will be stored + private String copyFilesFromServerDir; // location in EMS server whose contents will be copied to VM + private String copyFilesToClientDir; // location in VM where server files will be copied into + + private String clientTmpDir; // location of temp. directory in VM (typically /tmp) + private String serverTmpDir; // location of temp. directory in EMS server + private boolean keepTempFiles; // keep temporary files in EMS server (during debug) + + // ---------------------------------------------------- + + private boolean simulateConnection; + private boolean simulateExecution; + + private int maxRetries = 5; + private long retryDelay = 1000L; + private double retryBackoffFactor = 1.0; + + private long connectTimeout = 60000; + private long authenticateTimeout = 60000; + private long heartbeatInterval = 60000; + private long heartbeatReplyWait = heartbeatInterval; + private long commandExecutionTimeout = 60000; + + private final Map> instructions = new LinkedHashMap<>(); + private final Map parameters = new LinkedHashMap<>(); + + private boolean continueOnFail = false; + private String sessionRecordingDir = "logs"; + + // ---------------------------------------------------- + + private String clientInstallVarName = "__EMS_CLIENT_INSTALL__"; + private Pattern clientInstallSuccessPattern = Pattern.compile("^INSTALLED($|[\\s:=])", Pattern.CASE_INSENSITIVE); + private Pattern clientInstallErrorPattern = Pattern.compile("^ERROR($|[\\s:=])", Pattern.CASE_INSENSITIVE); + private boolean clientInstallSuccessIfVarIsMissing = false; + private boolean clientInstallErrorIfVarIsMissing = true; + + private String skipInstallVarName = "__EMS_CLIENT_INSTALL__"; + private Pattern skipInstallPattern = Pattern.compile("^SKIPPED($|[\\s:=])", Pattern.CASE_INSENSITIVE); + private boolean skipInstallIfVarIsMissing = false; + + private String ignoreNodeVarName = "__EMS_IGNORE_NODE__"; + private Pattern ignoreNodePattern = Pattern.compile("^IGNORED($|[\\s:=])", Pattern.CASE_INSENSITIVE); + private boolean ignoreNodeIfVarIsMissing = false; + + // ---------------------------------------------------- + + private List> installationContextProcessorPlugins = Collections.emptyList(); +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallationTask.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallationTask.java new file mode 100644 index 0000000..256b28a --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallationTask.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import gr.iccs.imu.ems.baguette.client.install.instruction.InstructionsSet; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.translate.TranslationContext; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * Client installation task + */ +@Data +@Builder +public class ClientInstallationTask { + private final String id; + private final String nodeId; + private final String name; + private final String os; + private final String address; + private final String type; + private final String provider; + private final SshConfig ssh; + private final NodeRegistryEntry nodeRegistryEntry; + private final List instructionSets; + private final TranslationContext translationContext; +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstaller.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstaller.java new file mode 100644 index 0000000..5e97f75 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstaller.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.common.plugin.PluginManager; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.validation.constraints.NotNull; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Client installer + */ +@Slf4j +@Service +@NoArgsConstructor +public class ClientInstaller implements InitializingBean { + private static ClientInstaller singleton; + + @Autowired + private ClientInstallationProperties properties; + @Autowired + private BaguetteServer baguetteServer; + @Autowired + private PluginManager pluginManager; + + private final AtomicLong taskCounter = new AtomicLong(); + private ExecutorService executorService; + + @Override + public void afterPropertiesSet() { + singleton = this; + executorService = Executors.newFixedThreadPool(properties.getWorkers()); + properties.getInstallationContextProcessorPlugins().forEach(pluginClass -> { + log.debug("ClientInstaller: Initializing plugin: {}", pluginClass); + pluginManager.initializePlugin(pluginClass); + }); + } + + public static ClientInstaller instance() { return singleton; } + + public void addTask(@NotNull ClientInstallationTask task) { + executorService.submit(() -> { + long taskCnt = taskCounter.getAndIncrement(); + try { + log.info("ClientInstaller: Executing Client installation Task #{}: task-id={}, node-id={}, name={}, type={}, address={}", + taskCnt, task.getId(), task.getNodeId(), task.getName(), task.getType(), task.getAddress()); + long startTm = System.currentTimeMillis(); + boolean result = executeTask(task, taskCnt); + long endTm = System.currentTimeMillis(); + log.info("ClientInstaller: Client installation Task #{}: result={}, duration={}ms", + taskCnt, result ? "SUCCESS" : "FAILED", endTm - startTm); + } catch (Throwable t) { + log.info("ClientInstaller: Exception caught in Client installation Task #{}: Exception: ", taskCnt, t); + } + }); + } + + private boolean executeTask(ClientInstallationTask task, long taskCounter) { + if (baguetteServer.getNodeRegistry().getCoordinator()==null) + throw new IllegalStateException("Baguette Server Coordinator has not yet been initialized"); + + if ("VM".equalsIgnoreCase(task.getType()) || "baremetal".equalsIgnoreCase(task.getType())) { + NodeRegistryEntry entry = baguetteServer.getNodeRegistry().getNodeByAddress(task.getAddress()); + if (entry==null) + throw new IllegalStateException("Node entry has been removed from Node Registry before installation: Node IP address: "+task.getAddress()); + //baguetteServer.handleNodeSituation(task.getAddress(), INTERNAL_ERROR); + entry.nodeInstalling(task); + + // Call InstallationContextPlugin's before installation + log.debug("ClientInstaller: PRE-INSTALLATION: Calling installation context processors: {}", properties.getInstallationContextProcessorPlugins()); + pluginManager.getActivePlugins(InstallationContextProcessorPlugin.class) + .forEach(plugin->((InstallationContextProcessorPlugin)plugin).processBeforeInstallation(task, taskCounter)); + + log.debug("ClientInstaller: INSTALLATION: Executing installation task: task-counter={}, task={}", taskCounter, task); + boolean success = executeVmTask(task, taskCounter); + log.debug("ClientInstaller: NODE_REGISTRY_ENTRY after installation execution: \n{}", task.getNodeRegistryEntry()); + + if (entry.getState()==NodeRegistryEntry.STATE.INSTALLING) { + log.warn("ClientInstaller: NODE_REGISTRY_ENTRY status is still INSTALLING after executing client installation. Changing to INSTALL_ERROR"); + entry.nodeInstallationError(null); + } + + // Call InstallationContextPlugin's after installation + log.debug("ClientInstaller: POST-INSTALLATION: Calling installation context processors: {}", properties.getInstallationContextProcessorPlugins()); + pluginManager.getActivePlugins(InstallationContextProcessorPlugin.class) + .forEach(plugin->((InstallationContextProcessorPlugin)plugin).processAfterInstallation(task, taskCounter, success)); + + // Pre-register Node to baguette Server Coordinator + log.debug("ClientInstaller: POST-INSTALLATION: Node is being pre-registered: {}", entry); + baguetteServer.getNodeRegistry().getCoordinator().preregister(entry); + + log.debug("ClientInstaller: Installation outcome: {}", success ? "Success" : "Error"); + return success; + } else { + log.error("ClientInstaller: UNSUPPORTED TASK TYPE: {}", task.getType()); + } + return false; + } + + private boolean executeVmTask(ClientInstallationTask task, long taskCounter) { + // Select the appropriate client installer plugin to run installation task. + // Currently, two installer plugins are available: SshClientInstaller, and SshJsClientInstaller + boolean result; + if (properties.getInstallerType()==ClientInstallationProperties.INSTALLER_TYPE.JS_INSTALLER) { + log.info("ClientInstaller: Using SshJsClientInstaller for task #{}", taskCounter); + result = SshJsClientInstaller.jsBuilder() + .task(task) + .taskCounter(taskCounter) + .properties(properties) + .build() + .execute(); + } else { + log.info("ClientInstaller: Using SshClientInstaller (default) for task #{}", taskCounter); + result = SshClientInstaller.builder() + .task(task) + .taskCounter(taskCounter) + /*.maxRetries(properties.getMaxRetries()) + .authenticationTimeout(properties.getAuthenticateTimeout()) + .connectTimeout(properties.getConnectTimeout()) + .heartbeatInterval(properties.getHeartbeatInterval()) + .simulateConnection(properties.isSimulateConnection()) + .simulateExecution(properties.isSimulateExecution()) + .commandExecutionTimeout(properties.getCommandExecutionTimeout())*/ + .properties(properties) + .build() + .execute(); + } + log.info("ClientInstaller: Task execution result #{}: success={}", taskCounter, result); + return result; + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallerPlugin.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallerPlugin.java new file mode 100644 index 0000000..2dc186b --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/ClientInstallerPlugin.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +public interface ClientInstallerPlugin { + default boolean execute() { + preProcessTask(); + boolean result = executeTask(); + result = result && postProcessTask(); + return result; + } + + void preProcessTask(); // Throw exception to block task execution + boolean executeTask(); + boolean postProcessTask(); +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/InstallationContextProcessorPlugin.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/InstallationContextProcessorPlugin.java new file mode 100644 index 0000000..3b77754 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/InstallationContextProcessorPlugin.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import gr.iccs.imu.ems.util.Plugin; + +public interface InstallationContextProcessorPlugin extends Plugin { + void processBeforeInstallation(ClientInstallationTask task, long taskCounter); + void processAfterInstallation(ClientInstallationTask task, long taskCounter, boolean success); +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshClientInstaller.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshClientInstaller.java new file mode 100644 index 0000000..c019c72 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshClientInstaller.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import gr.iccs.imu.ems.baguette.client.install.instruction.INSTRUCTION_RESULT; +import gr.iccs.imu.ems.baguette.client.install.instruction.Instruction; +import gr.iccs.imu.ems.baguette.client.install.instruction.InstructionsService; +import gr.iccs.imu.ems.baguette.client.install.instruction.InstructionsSet; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.channel.ChannelSession; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.mina.MinaServiceFactoryFactory; +import org.apache.sshd.scp.client.DefaultScpClientCreator; +import org.apache.sshd.scp.client.ScpClient; +import org.apache.sshd.scp.client.ScpClientCreator; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * SSH client installer + */ +@Slf4j +@Getter +public class SshClientInstaller implements ClientInstallerPlugin { + private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss.SSS"); + + private final ClientInstallationTask task; + private final long taskCounter; + + private final int maxRetries; + private final long retryDelay; + private final double retryBackoffFactor; + private final long connectTimeout; + private final long authenticationTimeout; + private final long heartbeatInterval; + private final long heartbeatReplyWait; + private final boolean simulateConnection; + private final boolean simulateExecution; + private final long commandExecutionTimeout; + private final boolean continueOnFail; + + private final ClientInstallationProperties properties; + + private SshClient sshClient; + //private SimpleClient simpleClient; + private ClientSession session; + //private ChannelShell shellChannel; + private StreamLogger streamLogger; + + @Builder + public SshClientInstaller(ClientInstallationTask task, long taskCounter, ClientInstallationProperties properties) { + this.task = task; + this.taskCounter = taskCounter; + + this.maxRetries = properties.getMaxRetries()>=0 ? properties.getMaxRetries() : 5; + this.retryDelay = properties.getRetryDelay()>0 ? properties.getRetryDelay() : 1000L; + this.retryBackoffFactor = properties.getRetryBackoffFactor()>0 ? properties.getRetryBackoffFactor() : 1.0; + + this.connectTimeout = properties.getConnectTimeout()>0 ? properties.getConnectTimeout() : 60000; + this.authenticationTimeout = properties.getAuthenticateTimeout()>0 ? properties.getAuthenticateTimeout() : 60000; + this.heartbeatInterval = properties.getHeartbeatInterval()>0 ? properties.getHeartbeatInterval() : 10000; + this.heartbeatReplyWait = properties.getHeartbeatReplyWait()>0 ? properties.getHeartbeatReplyWait() : 10 * heartbeatInterval; + this.simulateConnection = properties.isSimulateConnection(); + this.simulateExecution = properties.isSimulateExecution(); + this.commandExecutionTimeout = properties.getCommandExecutionTimeout()>0 ? properties.getCommandExecutionTimeout() : 120000; + this.continueOnFail = properties.isContinueOnFail(); + + this.properties = properties; + } + + @Override + public boolean executeTask(/*int retries*/) { + if (! openSshConnection()) + return false; + + boolean success; + try { + INSTRUCTION_RESULT exitResult = executeInstructionSets(); + success = exitResult != INSTRUCTION_RESULT.FAIL; + } catch (Exception ex) { + log.error("SshClientInstaller: Failed executing installation instructions for task #{}, Exception: ", taskCounter, ex); + success = false; + } + + if (success) log.info("SshClientInstaller: Task completed successfully #{}", taskCounter); + else log.info("SshClientInstaller: Error occurred while executing task #{}", taskCounter); + return closeSshConnection(success); + } + + protected boolean openSshConnection() { + task.getNodeRegistryEntry().nodeInstalling(task.getNodeRegistryEntry().getPreregistration()); + boolean success = false; + int retries = 0; + while (!success && retries<=maxRetries) { + if (retries>0) log.warn("SshClientInstaller: Retry {}/{} executing task #{}", retries, maxRetries, taskCounter); + try { + sshConnect(); + //sshOpenShell(); + success = true; + } catch (Exception ex) { + log.error("SshClientInstaller: Failed executing task #{}, Exception: ", taskCounter, ex); + retries++; + if (retries<=maxRetries) + waitToRetry(retries); + } + } + if (!success) { + log.error("SshClientInstaller: Giving up executing task #{} after {} retries", taskCounter, maxRetries); + return false; + } + return true; + } + + protected boolean closeSshConnection(boolean success) { + try { + //sshCloseShell(); + sshDisconnect(); + return success; + } catch (Exception ex) { + log.error("SshClientInstaller: Exception while disconnecting. Task #{}, Exception: ", taskCounter, ex); + return false; + } + } + + private void waitToRetry(int retries) { + long delay = Math.max(1, (long)(retryDelay * Math.pow(retryBackoffFactor, retries-1))); + try { + log.debug("SshClientInstaller: waitToRetry: Waiting for {}ms to retry", delay); + Thread.sleep(delay); + } catch (InterruptedException e) { + log.warn("SshClientInstaller: waitToRetry: Waiting to retry interrupted: ", e); + } + } + + private boolean sshConnect() throws Exception { + SshConfig config = task.getSsh(); + String host = config.getHost(); + int port = config.getPort(); + + if (simulateConnection) { + log.info("SshClientInstaller: Simulate connection to remote host: task #{}: host: {}:{}", taskCounter, host, port); + return true; + } + + // Get connection information + String privateKey = config.getPrivateKey(); + String fingerprint = config.getFingerprint(); + String username = config.getUsername(); + String password = config.getPassword(); + + // Create and configure SSH client + this.sshClient = SshClient.setUpDefaultClient(); + sshClient.setHostConfigEntryResolver(HostConfigEntryResolver.EMPTY); + sshClient.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE); + + //this.simpleClient = SshClient.wrapAsSimpleClient(sshClient); + //simpleClient.setConnectTimeout(connectTimeout); + //simpleClient.setAuthenticationTimeout(authenticationTimeout); + + // Set a huge idle timeout, keep-alive to true and heartbeat to configured value + PropertyResolverUtils.updateProperty(sshClient, CoreModuleProperties.HEARTBEAT_INTERVAL.getName(), heartbeatInterval); // Prevents server-side connection closing + PropertyResolverUtils.updateProperty(sshClient, CoreModuleProperties.HEARTBEAT_REPLY_WAIT.getName(), heartbeatReplyWait); // Prevents client-side connection closing + PropertyResolverUtils.updateProperty(sshClient, CoreModuleProperties.IDLE_TIMEOUT.getName(), Integer.MAX_VALUE); + PropertyResolverUtils.updateProperty(sshClient, CoreModuleProperties.SOCKET_KEEPALIVE.getName(), true); // Socket keep-alive at OS-level + log.debug("SshClientInstaller: Set IDLE_TIMEOUT to MAX, SOCKET-KEEP-ALIVE to true, and HEARTBEAT to {}", heartbeatInterval); + + // Explicitly set IO service factory factory to prevent conflict between MINA and Netty options + sshClient.setIoServiceFactoryFactory(new MinaServiceFactoryFactory()); + + // Start client and connect to SSH server + try { + sshClient.start(); + this.session = sshClient.connect(username, host, port) + .verify(connectTimeout) + .getSession(); + if (StringUtils.isNotBlank(privateKey)) { + PrivateKey privKey = getPrivateKey(privateKey); + //PublicKey pubKey = getPublicKey(publicKeyStr); + PublicKey pubKey = getPublicKey(privKey); + KeyPair keyPair = new KeyPair(pubKey, privKey); + session.addPublicKeyIdentity(keyPair); + } + if (StringUtils.isNotBlank(password)) { + session.addPasswordIdentity(password); + } + session.auth() + .verify(authenticationTimeout); + + // Initialize standard streams' logger + initStreamLogger(); + + log.info("SshClientInstaller: Connected to remote host: task #{}: host: {}:{}", taskCounter, host, port); + return true; + + } catch (Exception ex) { + log.error("SshClientInstaller: Error while connecting to remote host: task #{}: ", taskCounter, ex); + throw ex; + } + } + + private PrivateKey getPrivateKey(String pemStr) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory factory = KeyFactory.getInstance("RSA"); + try (StringReader keyReader = new StringReader(pemStr); PemReader pemReader = new PemReader(keyReader)) { + PemObject pemObject = pemReader.readPemObject(); + byte[] content = pemObject.getContent(); + PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(content); + PrivateKey privKey = factory.generatePrivate(keySpecPKCS8); + return privKey; + } + //PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(Base64.decode(privateKeyContent.replaceAll("\\n", "").replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", ""))); + //PrivateKey privKey = kf.generatePrivate(keySpecPKCS8); + } + + private PublicKey getPublicKey(PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory factory = KeyFactory.getInstance(privateKey.getAlgorithm()); + PKCS8EncodedKeySpec pubKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded()); + PublicKey publicKey = factory.generatePublic(pubKeySpec); + return publicKey; + } + + /*private PublicKey getPublicKey(String pemStr) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory factory = KeyFactory.getInstance("RSA"); + try (StringReader keyReader = new StringReader(pemStr); PemReader pemReader = new PemReader(keyReader)) { + PemObject pemObject = pemReader.readPemObject(); + byte[] content = pemObject.getContent(); + X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(content); + PublicKey publicKey = factory.generatePublic(pubKeySpec); + return publicKey; + } + //X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec( + // Base64.decode( + // pemStr.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "") + // .getBytes())); + //RSAPublicKey pubKey = (RSAPublicKey) factory.generatePublic(keySpecX509); + } + + private PublicKey getPublicKey(RSAPublicKeySpec rsaPrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory factory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = factory.generatePublic(new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent())); + return publicKey; + } + + private PublicKey getPublicKey(BCRSAPrivateCrtKey rsaPrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory factory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = factory.generatePublic(new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent())); + return publicKey; + }*/ + + private boolean sshDisconnect() throws Exception { + SshConfig config = task.getSsh(); + String host = config.getHost(); + int port = config.getPort(); + if (simulateConnection) { + log.info("SshClientInstaller: Simulate disconnect from remote host: task #{}: host: {}:{}", taskCounter, host, port); + return true; + } + + try { + if (streamLogger!=null) + streamLogger.close(); + + //channel.close(false).await(); + session.close(false); + //simpleClient.close(); + sshClient.stop(); + + log.info("SshClientInstaller: Disconnected from remote host: task #{}: host: {}:{}", taskCounter, host, port); + return true; + } catch (Exception ex) { + log.error("SshClientInstaller: Error while disconnecting from remote host: task #{}: ", taskCounter, ex); + throw ex; + } finally { + session = null; + //simpleClient = null; + sshClient = null; + } + } + + private void initStreamLogger() throws IOException { + if (streamLogger!=null) return; + + String address = session.getConnectAddress().toString().replace("/","").replace(":", "-"); + //log.trace("SshClientInstaller: address: {}", address); + String logFile = StringUtils.isNotBlank(properties.getSessionRecordingDir()) + ? properties.getSessionRecordingDir()+"/"+address+"-"+ simpleDateFormat.format(new Date())+"-"+taskCounter+".txt" + : null; + log.info("SshClientInstaller: Task #{}: Session will be recorded in file: {}", taskCounter, logFile); + this.streamLogger = new StreamLogger(logFile, " Task #"+taskCounter); + } + + private void setChannelStreams(ChannelSession channel) throws IOException { + initStreamLogger(); + channel.setIn( streamLogger.getIn() ); + channel.setOut( streamLogger.getOut() ); + channel.setErr( streamLogger.getErr() ); + } + + /*public boolean sshOpenShell() throws IOException { + if (simulateConnection) { + log.info("SshClientInstaller: Simulate open shell channel: task #{}", taskCounter); + return true; + } + + shellChannel = session.createShellChannel(); + setChannelStreams(shellChannel); + shellChannel.open().verify(connectTimeout); + //shellPipedIn = shellChannel.getInvertedIn(); + log.info("SshClientInstaller: Opened shell channel: task #{}", taskCounter); + + shellChannel.waitFor( + EnumSet.of(ClientChannelEvent.CLOSED), + authenticationTimeout); + //TimeUnit.SECONDS.toMillis(5)); + log.info("SshClientInstaller: Shell channel ready: task #{}", taskCounter); + + return true; + } + + public boolean sshCloseShell() throws IOException { + if (simulateConnection) { + log.info("SshClientInstaller: Simulate close shell channel: task #{}", taskCounter); + return true; + } + + shellChannel.close(); + shellChannel = null; + //shellPipedIn = null; + streamLogger.close(); + streamLogger = null; + log.info("SshClientInstaller: Closed shell channel: task #{}", taskCounter); + return true; + } + + public boolean sshShellExec(@NotNull String command, long executionTimeout) throws IOException { + if (simulateConnection || simulateExecution) { + log.info("SshClientInstaller: Simulate command execution: task #{}: command: {}", taskCounter, command); + return true; + } + + // Send command to remote side + if (!command.endsWith("\n")) + command += "\n"; + log.info("SshClientInstaller: Sending command: {}", command); + streamLogger.getInvertedIn().write(command.getBytes()); + streamLogger.getInvertedIn().flush(); + + // Search remote side output for expected patterns + // Not implemented + + shellChannel.waitFor( + EnumSet.of(ClientChannelEvent.CLOSED), + executionTimeout>0 ? executionTimeout : commandExecutionTimeout); + //TimeUnit.SECONDS.toMillis(5)); + return true; + }*/ + + public Integer sshExecCmd(String command) throws IOException { + return sshExecCmd(command, commandExecutionTimeout); + } + + public Integer sshExecCmd(String command, long executionTimeout) throws IOException { + if (simulateConnection || simulateExecution) { + log.info("SshClientInstaller: Simulate shell command execution: task #{}: command: {}", taskCounter, command); + return null; + } + + // Using EXEC channel + Integer exitStatus = null; + ChannelExec channel = session.createExecChannel(command); + setChannelStreams(channel); + log.debug("SshClientInstaller: task #{}: EXEC: New channel id: {}", taskCounter, channel.getChannelId()); + //streamLogger.getInvertedIn().write(command.getBytes()); + streamLogger.logMessage(String.format("EXEC: %s\n", command)); + try { + // Sending command to remote side + log.debug("SshClientInstaller: task #{}: EXEC: Sending command for execution: {} (connect timeout: {}ms)", taskCounter, command, connectTimeout); + session.resetIdleTimeout(); + channel.open().verify(connectTimeout); + log.trace("SshClientInstaller: task #{}: EXEC: Sending command verified: {}", taskCounter, command); + log.debug("SshClientInstaller: task #{}: EXEC: Opened channel id: {}", taskCounter, channel.getChannelId()); + + //XXX: TODO: Search remote side output for expected patterns + + // Wait until channel closes from server side (i.e. command completed) or timeout occurs + log.trace("SshClientInstaller: task #{}: EXEC: instruction execution-timeout: {}", taskCounter, executionTimeout); + log.trace("SshClientInstaller: task #{}: EXEC: default command-execution-timeout: {}", taskCounter, commandExecutionTimeout); + long execTimeout = executionTimeout != 0 ? executionTimeout : commandExecutionTimeout; + log.debug("SshClientInstaller: task #{}: EXEC: effective instruction execution-timeout: {}", taskCounter, execTimeout); + Set eventSet = channel.waitFor( + EnumSet.of(ClientChannelEvent.CLOSED), + execTimeout); + //TimeUnit.SECONDS.toMillis(50)); + log.debug("SshClientInstaller: task #{}: EXEC: Exit event set: {}", taskCounter, eventSet); + exitStatus = channel.getExitStatus(); + log.debug("SshClientInstaller: task #{}: EXEC: Exit status: {}", taskCounter, exitStatus); + } finally { + channel.close(); + } + + return exitStatus; + } + + public boolean sshFileDownload(String remoteFilePath, String localFilePath) throws IOException { + if (simulateConnection || simulateExecution) { + log.info("SshClientInstaller: Simulate file download: task #{}: remote: {} -> local: {}", taskCounter, remoteFilePath, localFilePath); + return true; + } + + streamLogger.logMessage(String.format("DOWNLOAD: SCP: %s -> %s\n", remoteFilePath, localFilePath)); + try { + log.info("SshClientInstaller: Downloading file: task #{}: remote: {} -> local: {}", taskCounter, remoteFilePath, localFilePath); + ScpClientCreator creator = new DefaultScpClientCreator(); + ScpClient scpClient = creator.createScpClient(session); + scpClient.download(remoteFilePath, localFilePath, ScpClient.Option.PreserveAttributes); + log.info("SshClientInstaller: File download completed: task #{}: remote: {} -> local: {}", taskCounter, remoteFilePath, localFilePath); + } catch (Exception ex) { + log.error("SshClientInstaller: File download failed: task #{}: remote: {} -> local: {} Exception: ", taskCounter, remoteFilePath, localFilePath, ex); + throw ex; + } + + return true; + } + + public boolean sshFileUpload(String localFilePath, String remoteFilePath) throws IOException { + if (simulateConnection || simulateExecution) { + log.info("SshClientInstaller: Simulate file upload: task #{}: local: {} -> remote: {}", taskCounter, localFilePath, remoteFilePath); + return true; + } + + streamLogger.logMessage(String.format("UPLOAD: SCP: %s -> %s\n", localFilePath, remoteFilePath)); + try { + long startTm = System.currentTimeMillis(); + log.info("SshClientInstaller: Uploading file: task #{}: local: {} -> remote: {}", taskCounter, localFilePath, remoteFilePath); + ScpClientCreator creator = new DefaultScpClientCreator(); + ScpClient scpClient = creator.createScpClient(session); + scpClient.upload(localFilePath, remoteFilePath, ScpClient.Option.PreserveAttributes); + long endTm = System.currentTimeMillis(); + log.info("SshClientInstaller: File upload completed in {}ms: task #{}: local: {} -> remote: {}", endTm-startTm, taskCounter, localFilePath, remoteFilePath); + } catch (Exception ex) { + log.error("SshClientInstaller: File upload failed: task #{}: local: {} -> remote: {} Exception: ", taskCounter, localFilePath, remoteFilePath, ex); + throw ex; + } + + return true; + } + + public boolean sshFileWrite(String content, String remoteFilePath, boolean isExecutable) throws IOException { + if (simulateConnection || simulateExecution) { + log.info("SshClientInstaller: Simulate file upload: task #{}: remote: {}, content-length={}", taskCounter, remoteFilePath, content.length()); + return true; + } + + streamLogger.logMessage(String.format("WRITE FILE: SCP: %s, content-length=%d \n", remoteFilePath, content.length())); + try { + long timestamp = System.currentTimeMillis(); + /*Collection permissions = isExecutable + ? Arrays.asList(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE) + : Arrays.asList(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); + log.info("SshClientInstaller: Uploading file: task #{}: remote: {}, perm={}, content-length={}", taskCounter, remoteFilePath, permissions, content.length()); + log.trace("SshClientInstaller: Uploading file: task #{}: remote: {}, perm={}, content:\n{}", taskCounter, remoteFilePath, permissions, content); + ScpClient scpClient = session.createScpClient(); + scpClient.upload(content.getBytes(), remoteFilePath, permissions, new ScpTimestamp(timestamp, timestamp)); + */ + + /* + The alternative approach next is much faster than the original approach above (commented out) + Old approach: write bytes directly to remote file + New approach: write contents to a local temp. file and then upload it to remote side + */ + + // Write contents to a temporary local file + File tmpDir = Paths.get(properties.getServerTmpDir()).toFile(); + tmpDir.mkdirs(); + File tmp = File.createTempFile("bci_upload_", ".tmp", tmpDir); + log.debug("SshClientInstaller: Write to temp. file: task #{}: temp-file: {}, remote: {}, content-length: {}", taskCounter, tmp, remoteFilePath, content.length()); + log.trace("SshClientInstaller: Write to temp. file: task #{}: temp-file: {}, remote: {}, content:\n{}", taskCounter, tmp, remoteFilePath, content); + try (FileWriter fw = new FileWriter(tmp.getAbsoluteFile())) { fw.write(content); } + + // Upload temporary local file to remote side + log.trace("SshClientInstaller: Call 'sshFileUpload': task #{}: temp-file={}, remote={}", taskCounter, tmp, remoteFilePath); + sshFileUpload(tmp.getAbsolutePath(), remoteFilePath); + + // Delete temporary file + if (!properties.isKeepTempFiles()) { + log.trace("SshClientInstaller: Remove temp. file: task #{}: temp-file={}", taskCounter, tmp); + tmp.delete(); + } + + long endTm = System.currentTimeMillis(); + log.info("SshClientInstaller: File upload completed in {}ms: task #{}: remote: {}, content-length={}", endTm-timestamp, taskCounter, remoteFilePath, content.length()); + log.trace("SshClientInstaller: File upload completed in {}ms: task #{}: remote: {}, content:\n{}", endTm-timestamp, taskCounter, remoteFilePath, content); + } catch (Exception ex) { + log.error("SshClientInstaller: File upload failed: task #{}: remote: {}, Exception: ", taskCounter, remoteFilePath, ex); + throw ex; + } + + return true; + } + + private INSTRUCTION_RESULT executeInstructionSets() throws IOException { + List instructionsSetList = task.getInstructionSets(); + INSTRUCTION_RESULT exitResult = INSTRUCTION_RESULT.SUCCESS; + int cntSuccess = 0; + int cntFail = 0; + for (InstructionsSet instructionsSet : instructionsSetList) { + log.info("\n ----------------------------------------------------------------------\n Task #{} : Instruction Set: {}", taskCounter, instructionsSet.getDescription()); + + // Check installation instructions condition + try { + if (! InstructionsService.getInstance().checkCondition(instructionsSet, task.getNodeRegistryEntry().getPreregistration())) { + log.info("SshClientInstaller: Task #{}: Installation Instructions set is skipped due to failed condition: {}", taskCounter, instructionsSet.getDescription()); + if (instructionsSet.isStopOnConditionFail()) { + log.info("SshClientInstaller: Task #{}: No further installation instructions sets will be executed due to stopOnConditionFail: {}", taskCounter, instructionsSet.getDescription()); + exitResult = INSTRUCTION_RESULT.FAIL; + break; + } + continue; + } + log.debug("SshClientInstaller: Task #{}: Condition evaluation for Installation Instructions Set succeeded: {}", taskCounter, instructionsSet.getDescription()); + } catch (Exception e) { + log.error("sshClientInstaller: Task #{}: Installation Instructions Set Condition evaluation error. Will not process remaining installation instructions sets: {}\n", taskCounter, instructionsSet.getDescription(), e); + exitResult = INSTRUCTION_RESULT.FAIL; + break; + } + + // Execute installation instructions + log.info("SshClientInstaller: Task #{}: Executing installation instructions set: {}", taskCounter, instructionsSet.getDescription()); + streamLogger.logMessage( + String.format("\n ----------------------------------------------------------------------\n Task #%d : Executing instruction set: %s\n", + taskCounter, instructionsSet.getDescription())); + INSTRUCTION_RESULT result = executeInstructions(instructionsSet); + if (result==INSTRUCTION_RESULT.FAIL) { + log.error("SshClientInstaller: Task #{}: Installation Instructions set failed: {}", taskCounter, instructionsSet.getDescription()); + cntFail++; + if (!continueOnFail) { + exitResult = INSTRUCTION_RESULT.FAIL; + break; + } + } else + if (result==INSTRUCTION_RESULT.EXIT) { + log.info("SshClientInstaller: Task #{}: Instruction set processing exits", taskCounter); + cntSuccess++; + exitResult = INSTRUCTION_RESULT.EXIT; + break; + } else { + log.info("SshClientInstaller: Task #{}: Installation Instructions set succeeded: {}", taskCounter, instructionsSet.getDescription()); + cntSuccess++; + } + } + log.info("\n -------------------------------------------------------------------------\n Task #{} : Instruction sets processed: successful={}, failed={}, exit-result={}", taskCounter, cntSuccess, cntFail, exitResult); + return exitResult; + } + + private INSTRUCTION_RESULT executeInstructions(InstructionsSet instructionsSet) throws IOException { + Map valueMap = task.getNodeRegistryEntry().getPreregistration(); + int numOfInstructions = instructionsSet.getInstructions().size(); + int cnt = 0; + int insCount = instructionsSet.getInstructions().size(); + for (Instruction ins : instructionsSet.getInstructions()) { + if (ins==null) continue; + cnt++; + + // Check instruction condition + try { + if (! InstructionsService.getInstance().checkCondition(ins, valueMap)) { + log.info("SshClientInstaller: Task #{}: Instruction is skipped due to failed condition {}/{}: {}", taskCounter, cnt, numOfInstructions, ins.description()); + if (ins.isStopOnConditionFail()) { + log.info("SshClientInstaller: Task #{}: No further instructions will be executed due to stopOnConditionFail: {}/{}: {}", taskCounter, cnt, numOfInstructions, ins.description()); + return INSTRUCTION_RESULT.FAIL; + } + continue; + } + log.debug("SshClientInstaller: Task #{}: Condition evaluation for instruction succeeded: {}/{}: {}", taskCounter, cnt, numOfInstructions, ins.description()); + } catch (Exception e) { + log.error("sshClientInstaller: Task #{}: Instruction Condition evaluation error. Will not process remaining instructions: {}/{}: {}\n", taskCounter, cnt, numOfInstructions, ins.description(), e); + return INSTRUCTION_RESULT.FAIL; + } + + // Execute instruction + ins = InstructionsService + .getInstance() + .resolvePlaceholders(ins, valueMap); + log.trace("SshClientInstaller: Task #{}: Executing instruction {}/{}: {}", taskCounter, cnt, numOfInstructions, ins); + log.info("SshClientInstaller: Task #{}: Executing instruction {}/{}: {}", taskCounter, cnt, numOfInstructions, ins.description()); + Integer exitStatus; + boolean result = true; + switch (ins.taskType()) { + case LOG: + log.info("SshClientInstaller: Task #{}: LOG: {}", taskCounter, ins.message()); + break; + case CMD: + log.info("SshClientInstaller: Task #{}: EXEC: {}", taskCounter, ins.command()); + int retries = 0; + int maxRetries = ins.retries(); + while (true) { + try { + exitStatus = sshExecCmd(ins.command(), ins.executionTimeout()); + result = (exitStatus!=null); + //result = (exitStatus==0); + log.info("SshClientInstaller: Task #{}: EXEC: exit-status={}", taskCounter, exitStatus); + if (result) break; + } catch (Exception ex) { + if (retries+1>=maxRetries) + throw ex; + else + log.error("SshClientInstaller: Task #{}: EXEC: Last command raised exception: ", taskCounter, ex); + } + + retries++; + if (retries<=maxRetries) { + log.info("SshClientInstaller: Task #{}: Retry {}/{} for instruction {}/{}: {}", + taskCounter, retries, maxRetries, cnt, numOfInstructions, ins.description()); + } else { + if (maxRetries>0) + log.error("sshClientInstaller: Task #{}: Last instruction failed {} times. Giving up", taskCounter, maxRetries); + result = false; + break; + } + } + break; + /*case SHELL: + log.info("SshClientInstaller: Task #{}: SHELL: {}", taskCounter, ins.getCommand()); + retries = 0; + maxRetries = ins.getRetries(); + while (true) { + try { + result = sshShellExec(ins.getCommand(), ins.getExecutionTimeout()); + log.info("SshClientInstaller: Task #{}: SHELL: exit-status={}", taskCounter, result); + if (result) break; + } catch (Exception ex) { + if (retries+1>=maxRetries) + throw ex; + else + log.error("SshClientInstaller: Task #{}: SHELL: Last command raised exception: ", taskCounter, ex); + } + + retries++; + if (retries<=maxRetries) { + log.info("SshClientInstaller: Task #{}: Retry {}/{} for instruction {}/{}: {}", + taskCounter, retries, maxRetries, cnt, numOfInstructions, ins.getDescription()); + } else { + if (maxRetries>0) + log.error("sshClientInstaller: Task #{}: Last instruction failed {} times. Giving up", taskCounter, maxRetries); + result = false; + break; + } + } + break;*/ + case FILE: + //log.info("SshClientInstaller: Task #{}: FILE: {}, content-length={}", taskCounter, ins.getFileName(), ins.getContents().length()); + if (Paths.get(ins.localFileName()).toFile().isDirectory()) { + log.info("SshClientInstaller: Task #{}: FILE: COPY-PROCESS DIR: {} -> {}", taskCounter, ins.localFileName(), ins.fileName()); + result = copyDir(ins.localFileName(), ins.fileName(), valueMap); + } else + if (Paths.get(ins.localFileName()).toFile().isFile()) { + log.info("SshClientInstaller: Task #{}: FILE: COPY-PROCESS FILE: {} -> {}", taskCounter, ins.localFileName(), ins.fileName()); + Path sourceFile = Paths.get(ins.localFileName()); + Path sourceBaseDir = Paths.get(ins.localFileName()).getParent(); + result = copyFile(sourceFile, sourceBaseDir, ins.fileName(), valueMap, ins.executable()); + } else { + log.error("SshClientInstaller: Task #{}: FILE: ERROR: Local file is not directory or normal file: {}", taskCounter, ins.localFileName()); + result = false; + } + break; + case COPY: + case UPLOAD: + log.info("SshClientInstaller: Task #{}: UPLOAD: {} -> {}", taskCounter, ins.localFileName(), ins.fileName()); + result = sshFileUpload(ins.localFileName(), ins.fileName()); + break; + case DOWNLOAD: + log.info("SshClientInstaller: Task #{}: DOWNLOAD: {} -> {}", taskCounter, ins.fileName(), ins.localFileName()); + result = sshFileDownload(ins.fileName(), ins.localFileName()); + if (result) + result = processPatterns(ins, valueMap); + break; + case CHECK: + log.info("SshClientInstaller: Task #{}: CHECK: {}", taskCounter, ins.command()); + exitStatus = sshExecCmd(ins.command()); + log.info("SshClientInstaller: Task #{}: CHECK: exit-status={}", taskCounter, exitStatus); + log.debug("SshClientInstaller: Task #{}: CHECK: Result: match={}, match-status={}, exec-status={}", + taskCounter, ins.match(), ins.exitCode(), exitStatus); + if (ins.match() && exitStatus==ins.exitCode() + || !ins.match() && exitStatus!=ins.exitCode()) + { + log.info("SshClientInstaller: Task #{}: CHECK: MATCH: {}", taskCounter, ins.message()); + log.info("SshClientInstaller: Task #{}: CHECK: MATCH: Will not process more instructions", taskCounter); + return INSTRUCTION_RESULT.SUCCESS; + } + break; + + case SET_VARS: + log.info("SshClientInstaller: Task #{}: SET_VARS:", taskCounter); + if (ins.variables()!=null && ins.variables().size()>0) { + ins.variables().forEach((varName, varExpression) -> { + try { + String varValue = InstructionsService.getInstance().processPlaceholders(varExpression, valueMap); + log.info("SshClientInstaller: Task #{}: Setting VAR: {} = {}", taskCounter, varName, varValue); + valueMap.put(varName, varValue); + } catch (Exception e) { + log.error("SshClientInstaller: Task #{}: ERROR while Setting VAR: {}: {}\n", taskCounter, varName, varExpression, e); + } + }); + } else + log.warn("SshClientInstaller: Task #{}: SET_VARS: No variables specified", taskCounter); + break; + case UNSET_VARS: + log.info("SshClientInstaller: Task #{}: UNSET_VARS:", taskCounter); + if (ins.variables()!=null && ins.variables().size()>0) { + Set vars = ins.variables().keySet(); + log.info("SshClientInstaller: Task #{}: Unsetting VAR: {}", taskCounter, vars); + valueMap.keySet().removeAll(vars); + } else + log.warn("SshClientInstaller: Task #{}: UNSET_VARS: No variables specified", taskCounter); + break; + case PRINT_VARS: + //log.info("SshClientInstaller: Task #{}: PRINT_VARS:", taskCounter); + String output = valueMap.entrySet().stream() + .map(e -> " VAR: "+e.getKey()+" = "+e.getValue()) + .collect(Collectors.joining("\n")); + log.info("SshClientInstaller: Task #{}: PRINT_VARS:\n{}", taskCounter, output); + break; + case EXIT_SET: + log.info("SshClientInstaller: Task #{}: EXIT_SET: Stop this instruction set processing", taskCounter); + try { + if (StringUtils.isNotBlank(ins.command())) { + String exitResult = ins.command().trim().toUpperCase(); + log.info("SshClientInstaller: Task #{}: EXIT_SET: Result={}", taskCounter, exitResult); + return INSTRUCTION_RESULT.valueOf(exitResult); + } + } catch (Exception e) { + log.error("SshClientInstaller: Task #{}: EXIT_SET: Invalid EXIT_SET result: {}. Will return FAIL", taskCounter, ins.command()); + return INSTRUCTION_RESULT.FAIL; + } + log.info("SshClientInstaller: Task #{}: EXIT_SET: Result={}", taskCounter, INSTRUCTION_RESULT.SUCCESS); + return INSTRUCTION_RESULT.SUCCESS; + case EXIT: + log.info("SshClientInstaller: Task #{}: EXIT: Stop any further instruction processing", taskCounter); + return INSTRUCTION_RESULT.EXIT; + default: + log.error("sshClientInstaller: Unknown instruction type. Ignoring it: {}", ins); + } + if (!result) { + log.error("sshClientInstaller: Last instruction failed. Will not process remaining instructions"); + return INSTRUCTION_RESULT.FAIL; + } + + if (cnt valueMap) throws IOException { + // Copy files from EMS server to Baguette Client + if (StringUtils.isNotEmpty(sourceDir) && StringUtils.isNotEmpty(targetDir)) { + Path baseDir = Paths.get(sourceDir).toAbsolutePath(); + try (Stream stream = Files.walk(baseDir, Integer.MAX_VALUE)) { + List paths = stream + .filter(Files::isRegularFile) + .map(Path::toAbsolutePath) + .sorted() + .collect(Collectors.toList()); + for (Path p : paths) { + if (!copyFile(p, baseDir, targetDir, valueMap, false)) + return false; + } + } + } + return true; + } + + public boolean copyFile(Path sourcePath, Path sourceBaseDir, String targetDir, Map valueMap, boolean isExecutable) throws IOException { + String targetFile = StringUtils.substringAfter(sourcePath.toUri().toString(), sourceBaseDir.toUri().toString()); + if (!targetFile.startsWith("/")) targetFile = "/"+targetFile; + targetFile = targetDir + targetFile; + + String contents = new String(Files.readAllBytes(sourcePath)); + log.info("SshClientInstaller: Task #{}: FILE: {}, content-length={}", taskCounter, targetFile, contents.length()); + contents = StringSubstitutor.replace(contents, valueMap); + log.trace("SshClientInstaller: Task #{}: FILE: {}, final-content:\n{}", taskCounter, targetFile, contents); + + String description = String.format("Copy file from server to temp to client: %s -> %s", sourcePath.toString(), targetFile); + + return sshFileWrite(contents, targetFile, isExecutable); + } + + private boolean processPatterns(Instruction ins, Map valueMap) { + Map patterns = ins.patterns(); + if (patterns==null || patterns.size()==0) { + log.info("SshClientInstaller: processPatterns: No patterns to process"); + return true; + } + + // Read local file + String[] linesArr; + try (Stream lines = Files.lines(Paths.get(ins.localFileName()))) { + linesArr = lines.toArray(String[]::new); + } catch (IOException e) { + log.error("SshClientInstaller: processPatterns: Error while reading local file: {} -- Exception: ", ins.localFileName(), e); + return false; + } + + // Process file lines against instruction patterns + patterns.forEach((varName,pattern) -> { + log.trace("SshClientInstaller: processPatterns: For-Each: var-name={}, pattern={}, pattern-flags={}", varName, pattern, pattern.flags()); + Matcher matcher = null; + for (String line : linesArr) { + Matcher m = pattern.matcher(line); + if (m.matches()) { + matcher = m; + //break; // Uncomment to return the first match. Comment to return the last match. + } + } + if (matcher!=null && matcher.matches()) { + String varValue = matcher.group( matcher.groupCount()>0 ? 1 : 0 ); + log.info("SshClientInstaller: processPatterns: Setting variable '{}' to: {}", varName, varValue); + valueMap.put(varName, varValue); + } else { + log.info("SshClientInstaller: processPatterns: No match for variable '{}' with pattern: {}", varName, pattern); + } + }); + + return true; + } + + @Override + public void preProcessTask() { + // Throw exception to prevent task exception, if task data have problem + } + + @Override + public boolean postProcessTask() { + log.trace("SshClientInstaller: postProcessTask: BEGIN:\n{}", task.getNodeRegistryEntry().getPreregistration()); + + // Check if Baguette client has been installed (or failed to install) + log.trace("SshClientInstaller: postProcessTask: CLIENT INSTALLATION...."); + boolean result = postProcessVariable( + properties.getClientInstallVarName(), + properties.getClientInstallSuccessPattern(), + value -> { task.getNodeRegistryEntry().nodeInstallationComplete(value); return true; }, + null, null); + log.trace("SshClientInstaller: postProcessTask: CLIENT INSTALLATION.... result: {}", result); + if (result) return true; + + // Check if Baguette client installation has failed + log.trace("SshClientInstaller: postProcessTask: CLIENT INSTALLATION FAILED...."); + result = postProcessVariable( + properties.getClientInstallVarName(), + properties.getClientInstallErrorPattern(), + value -> { task.getNodeRegistryEntry().nodeInstallationError(value); return true; }, + null, null); + log.trace("SshClientInstaller: postProcessTask: CLIENT INSTALLATION.... result: {}", result); + if (result) return true; + + // Check if Baguette client installation has been skipped (not attempted at all) + log.trace("SshClientInstaller: postProcessTask: CLIENT INSTALLATION SKIP...."); + result = postProcessVariable( + properties.getSkipInstallVarName(), + properties.getSkipInstallPattern(), + value -> { task.getNodeRegistryEntry().nodeNotInstalled(value); return true; }, + null, null); + log.trace("SshClientInstaller: postProcessTask: CLIENT INSTALLATION SKIP.... result: {}", result); + if (result) return true; + + // Check if the Node must be ignored by EMS + log.trace("SshClientInstaller: postProcessTask: NODE IGNORE...."); + result = postProcessVariable( + properties.getIgnoreNodeVarName(), + properties.getIgnoreNodePattern(), + value -> { task.getNodeRegistryEntry().nodeIgnore(value); return true; }, + null, null); + log.trace("SshClientInstaller: postProcessTask: NODE IGNORE.... result: {}", result); + if (result) return true; + + // Process defaults, if variables are missing or inconclusive + log.trace("SshClientInstaller: postProcessTask: DEFAULTS...."); + if (properties.isIgnoreNodeIfVarIsMissing()) { + log.trace("SshClientInstaller: postProcessTask: DEFAULTS.... NODE IGNORED"); + task.getNodeRegistryEntry().nodeIgnore(null); + } else + if (properties.isSkipInstallIfVarIsMissing()) { + log.trace("SshClientInstaller: postProcessTask: DEFAULTS.... CLIENT INSTALLATION SKIPPED"); + task.getNodeRegistryEntry().nodeNotInstalled(null); + } else + if (properties.isClientInstallSuccessIfVarIsMissing()) { + log.trace("SshClientInstaller: postProcessTask: DEFAULTS.... CLIENT INSTALLED"); + task.getNodeRegistryEntry().nodeInstallationComplete(null); + } else + if (properties.isClientInstallErrorIfVarIsMissing()) { + log.trace("SshClientInstaller: postProcessTask: DEFAULTS.... CLIENT INSTALLATION ERROR"); + task.getNodeRegistryEntry().nodeInstallationError(null); + } else + log.trace("SshClientInstaller: postProcessTask: DEFAULTS.... NO DEFAULT"); + log.trace("SshClientInstaller: postProcessTask: END"); + return true; + } + + private boolean postProcessVariable(String varName, Pattern pattern, @NonNull Function match, Function notMatch, Supplier missing) { + log.trace("SshClientInstaller: postProcessVariable: var={}, pattern={}", varName, pattern); + if (StringUtils.isNotBlank(varName) && pattern!=null) { + String value = task.getNodeRegistryEntry().getPreregistration().get(varName); + log.trace("SshClientInstaller: postProcessVariable: var={}, value={}", varName, value); + if (value!=null) { + if (pattern.matcher(value).matches()) { + log.trace("SshClientInstaller: postProcessVariable: MATCH-END: var={}, value={}, pattern={}", varName, value, pattern); + return match.apply(value); + } else { + log.trace("SshClientInstaller: postProcessVariable: NO MATCH: var={}, value={}, pattern={}", varName, value, pattern); + if (notMatch!=null) { + log.trace("SshClientInstaller: postProcessVariable: NO MATCH-END: var={}, value={}, pattern={}", varName, value, pattern); + return notMatch.apply(value); + } + } + } + } + if (missing!=null) { + log.trace("SshClientInstaller: postProcessVariable: DEFAULT-END: var={}", varName); + return missing.get(); + } + log.trace("SshClientInstaller: postProcessVariable: False-END: var={}", varName); + return false; + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshConfig.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshConfig.java new file mode 100644 index 0000000..489a3f7 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +/** + * SSH connection information + */ +@Data +@Builder +@ToString(exclude = {"password", "privateKey"}) +public class SshConfig { + private String host; + @Builder.Default + private int port = 22; + private String username; + private String password; + private String privateKey; + private String fingerprint; +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshJsClientInstaller.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshJsClientInstaller.java new file mode 100644 index 0000000..ddc2722 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/SshJsClientInstaller.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import gr.iccs.imu.ems.baguette.client.install.instruction.INSTRUCTION_RESULT; +import gr.iccs.imu.ems.baguette.client.install.instruction.InstructionsSet; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.ResourceUtils; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * SSH-Javascript client installer + */ +@Slf4j +public class SshJsClientInstaller extends SshClientInstaller { + + @Builder(builderMethodName = "jsBuilder") + public SshJsClientInstaller(ClientInstallationTask task, long taskCounter, ClientInstallationProperties properties) { + super(task, taskCounter, properties); + } + + @Override + public boolean executeTask() { + log.info("SshJsClientInstaller: Task #{}: Opening SSH connection...", getTaskCounter()); + if (!openSshConnection()) { + return false; + } + + boolean success; + try { + log.info("SshJsClientInstaller: Task #{}: Executing JS installation scripts...", getTaskCounter()); + INSTRUCTION_RESULT exitResult = executeJsScripts(); + success = exitResult != INSTRUCTION_RESULT.FAIL; + } catch (Exception ex) { + log.error("SshJsClientInstaller: Task #{}: Exception while executing JS installation scripts: ", getTaskCounter(), ex); + success = false; + } + + log.info("SshJsClientInstaller: Task #{}: Closing SSH connection...", getTaskCounter()); + return closeSshConnection(success); + } + + private INSTRUCTION_RESULT executeJsScripts() throws IOException { + List jsScriptList = Optional.ofNullable(getTask().getInstructionSets()) + .orElseThrow(() -> new IllegalArgumentException("No SSH-Javascript installer scripts configured")) + .stream() + .map(InstructionsSet::getFileName) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); + log.debug("SshJsClientInstaller: Task #{}: Configured installation scripts: {}", getTaskCounter(), jsScriptList); + if (jsScriptList.isEmpty()) + throw new IllegalArgumentException("SSH-Javascript installation scripts are blank"); + + INSTRUCTION_RESULT exitResult = null; + int cntSuccess = 0; + int cntFail = 0; + for (String jsScript : jsScriptList) { + log.info("\n ----------------------------------------------------------------------\n Task #{} : JS installation script: {}", getTaskCounter(), jsScript); + + // Execute JS installation script + getStreamLogger().logMessage( + String.format("\n ----------------------------------------------------------------------\n Task #%d : JS installation script: %s\n", + getTaskCounter(), jsScript)); + + INSTRUCTION_RESULT result = executeJsScript(jsScript); + + if (result==INSTRUCTION_RESULT.FAIL) { + log.error("SshJsClientInstaller: Task #{}: JS installation script failed: {}", getTaskCounter(), jsScript); + getStreamLogger().logMessage( + String.format("\n Task #%d : JS installation script failed: %s\n", getTaskCounter(), jsScript)); + cntFail++; + exitResult = INSTRUCTION_RESULT.FAIL; + if (!isContinueOnFail()) { + break; + } + } else + if (result==INSTRUCTION_RESULT.EXIT) { + log.info("SshJsClientInstaller: Task #{}: JS installation script processing exits", getTaskCounter()); + getStreamLogger().logMessage( + String.format("\n Task #%d : JS installation script processing exits\n", getTaskCounter())); + cntSuccess++; + exitResult = INSTRUCTION_RESULT.EXIT; + break; + } else { + log.info("SshJsClientInstaller: Task #{}: JS installation script succeeded: {}", getTaskCounter(), jsScript); + getStreamLogger().logMessage( + String.format("\n Task #%d : JS installation script succeeded: %s\n", getTaskCounter(), jsScript)); + cntSuccess++; + exitResult = INSTRUCTION_RESULT.SUCCESS; + } + } + log.info("\n -------------------------------------------------------------------------\n Task #{} : JS installation scripts processed: successful={}, failed={}, exit-result={}", getTaskCounter(), cntSuccess, cntFail, exitResult); + getStreamLogger().logMessage( + String.format("\n ----------------------------------------------------------------------\n Task #%d : JS installation scripts processed: successful=%d, failed=%d, exit-result=%s\n", getTaskCounter(), cntSuccess, cntFail, exitResult)); + return exitResult; + } + + public void printAndLog(Object args) { + try { + String message; + if (args==null) { + message = "null"; + } else + if (args.getClass().isArray()) { + message = Arrays.stream((Object[]) args) + .map(o -> o == null ? "null" : o.toString()) + .collect(Collectors.joining(" ")); + } else { + message = args.toString(); + } + if (!message.endsWith("\n")) message += "\n"; +// getStreamLogger().getOut().write(String.format(message).getBytes()); + getStreamLogger().logMessage(message); + } catch (IOException e) { + log.error("SshJsClientInstaller: printAndLog: ", e); + } + } + + private INSTRUCTION_RESULT executeJsScript(String jsScript) { + try { + // Initializing JS engine + log.debug("SshJsClientInstaller: Task #{}: Initializing JS engine", getTaskCounter()); + ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); + ScriptEngine engine = scriptEngineManager.getEngineByName("nashorn"); + engine.getContext().getBindings(ScriptContext.GLOBAL_SCOPE).put("installer", this); + engine.getContext().getBindings(ScriptContext.GLOBAL_SCOPE).put("log", (Consumer)this::printAndLog); + + log.debug("SshJsClientInstaller: Task #{}: Executing JS script: {}", getTaskCounter(), jsScript); + File jsFile = ResourceUtils.getFile(jsScript); + log.trace("SshJsClientInstaller: Task #{}: JS script file: {}", getTaskCounter(), jsFile); + Object result = engine.eval(new FileReader(jsFile)); + + if (result==null) { + log.error("SshJsClientInstaller: Task #{}: JS installation script returned NULL: {}", getTaskCounter(), jsScript); + return INSTRUCTION_RESULT.FAIL; + } + if (result instanceof Integer) { + int code = (int)result; + log.info("SshJsClientInstaller: Task #{}: JS installation script returned: code={}, script: {}", getTaskCounter(), code, jsScript); + return code==0 ? INSTRUCTION_RESULT.SUCCESS : INSTRUCTION_RESULT.FAIL; + } else { + log.error("SshJsClientInstaller: Task #{}: JS installation script returned NON-INTEGER value: {}, script: {}", getTaskCounter(), result, jsScript); + return INSTRUCTION_RESULT.FAIL; + } + } catch (ScriptException | IOException e) { + log.error("SshJsClientInstaller: Task #{}: Exception while executing script: {}, Exception: ", getTaskCounter(), jsScript, e); + return INSTRUCTION_RESULT.FAIL; + } + } + + public String getInstallationResult() { + return getTask().getNodeRegistryEntry().getPreregistration().get(getProperties().getClientInstallVarName()); + } + + public void setInstallationResult(boolean success) { + getTask().getNodeRegistryEntry().getPreregistration().put( + getProperties().getClientInstallVarName(), + success ? "INSTALLED" : "ERROR"); + } + + public void clearInstallationResult() { + getTask().getNodeRegistryEntry().getPreregistration().remove( + getProperties().getClientInstallVarName()); + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/StreamLogger.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/StreamLogger.java new file mode 100644 index 0000000..101717f --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/StreamLogger.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.*; +import java.util.Arrays; +import java.util.Objects; + +/** + * Logs and formats In/Out/Err streams + */ +@Slf4j +public class StreamLogger { + private final FileOutputStream fos; + private final PipedOutputStream pos; + private final PipedInputStream pis; + private final MonitorOutputStream mos; + + private final OutputStream ncInvertedIn; + private final InputStream ncIn; + private final OutputStream ncOut; + private final OutputStream ncErr; + + private String lastLine; + private long lastLineTime; + + public StreamLogger(String logFile) throws IOException { + this(logFile, ""); + } + + public StreamLogger(String logFile, String prefix) throws IOException { + this.fos = StringUtils.isNotBlank(logFile) ? new FileOutputStream(logFile) : null; + this.pos = new PipedOutputStream(); + this.pis = new PipedInputStream(pos); + this.mos = new MonitorOutputStream(this); + + this.ncIn = new LoggerInputStream(pis, prefix+" IN", toArray(System.out, fos)); + this.ncInvertedIn = pos; + this.ncOut = new LoggerOutputStream(prefix+" OUT", toArray(System.out, mos, fos)); + this.ncErr = new LoggerOutputStream(prefix+" ERR", toArray(System.err, fos)); + } + + private OutputStream[] toArray(OutputStream...streams) { + return Arrays.stream(streams) + .filter(Objects::nonNull) + .toArray(OutputStream[]::new); + } + + public InputStream getIn() { return ncIn; } + + public OutputStream getInvertedIn() { + return ncInvertedIn; + } + + public OutputStream getOut() { + return ncOut; + } + + public OutputStream getErr() { + return ncErr; + } + + public void close() throws IOException { + if (fos!=null) fos.close(); + pos.close(); + } + + public void logMessage(String message) throws IOException { + if (fos!=null) fos.write(message.getBytes()); + } + + private void newLine(String line, long timestamp) { + lastLine = line; + lastLineTime = timestamp; + } + + static class LoggerInputStream extends InputStream { + private final InputStream in; + private final OutputStream[] streams; + private final byte[] prefix; + + public LoggerInputStream(InputStream in, String prefix, OutputStream...streams) { + this.in = in; + this.prefix = (prefix+"< ").getBytes(); + this.streams = streams; + } + + @Override + public int read() throws IOException { + int b = in.read(); + writeToStreams(b); + return b; + } + + private void writeToStreams(int b) throws IOException { + for (int i=0; i ").getBytes(); + this.streams = streams; + } + + @Override + public void write(int b) throws IOException { + if (newline) { + writeToStreams(prefix); + newline = false; + } + writeToStreams(b); + if (b=='\n') newline = true; + } + + private void writeToStreams(int b) throws IOException { + for (int i=0; i, InstallationHelper { + protected static AbstractInstallationHelper instance = null; + protected static List LINUX_OS_FAMILIES; + protected static List WINDOWS_OS_FAMILIES; + + @Autowired + @Getter @Setter + protected ClientInstallationProperties properties; + @Autowired + protected PasswordUtil passwordUtil; + + protected String archiveBase64; + protected boolean isServerSecure; + protected String serverCert; + + public synchronized static AbstractInstallationHelper getInstance() { + return instance; + } + + @Override + public void afterPropertiesSet() { + log.debug("AbstractInstallationHelper.afterPropertiesSet(): class={}: configuration: {}", getClass().getName(), properties); + AbstractInstallationHelper.instance = this; + LINUX_OS_FAMILIES = properties.getOsFamilies().get("LINUX"); + WINDOWS_OS_FAMILIES = properties.getOsFamilies().get("WINDOWS"); + } + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + log.debug("AbstractInstallationHelper.onApplicationEvent(): event={}", event); + TomcatWebServer tomcat = (TomcatWebServer) event.getSource(); + + try { + initServerCertificateFile(tomcat); + initBaguetteClientConfigArchive(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void initServerCertificateFile(TomcatWebServer tomcat) throws Exception { + this.isServerSecure = tomcat.getTomcat().getConnector().getSecure(); + log.debug("AbstractInstallationHelper.initServerCertificate(): Embedded Tomcat is secure: {}", isServerSecure); + + if (isServerSecure) { + // If HTTPS is enabled + SSLHostConfig[] sslHostConfigArr = tomcat.getTomcat().getConnector().findSslHostConfigs(); + if (log.isDebugEnabled()) + log.debug("AbstractInstallationHelper.initServerCertificate(): Tomcat SSL host config array: {}", Arrays.asList(sslHostConfigArr)); + if (sslHostConfigArr.length!=1) + throw new RuntimeException("Embedded Tomcat has zero or more than one SSL host configurations: "+sslHostConfigArr.length); + + // Get certificate entries (in key manager/store) for this SSL Hosting configuration + Set sslCertificatesSet = sslHostConfigArr[0].getCertificates(); + log.debug("AbstractInstallationHelper.initServerCertificate(): SSL certificates set: {}", sslCertificatesSet); + int n = 0; + String serverCert = null; + for (SSLHostConfigCertificate sslCertificate : sslCertificatesSet) { + // Get entry alias + log.debug("AbstractInstallationHelper.initServerCertificate(): SSL certificate[{}]: {}", n, sslCertificate); + String keyAlias = sslCertificate.getCertificateKeyAlias(); + log.debug("AbstractInstallationHelper.initServerCertificate(): SSL certificate[{}]: alias={}", n, keyAlias); + + // Get certificate chain for entry with 'alias' + X509Certificate[] chain = sslCertificate.getSslContext().getCertificateChain(keyAlias); + StringBuilder sb = new StringBuilder(); + int m = 0; + for (X509Certificate c : chain) { + // Export certificate in PEM format (for each chain item) + String certPem = KeystoreUtil.exportCertificateAsPEM(c); + log.debug("AbstractInstallationHelper.initServerCertificate(): SSL certificate[{}]: {}: \n{}", n, m, certPem); + // Append PEM certificate to 'sb' + sb.append(certPem).append(System.getProperty("line.separator")); + m++; + } + // The first entry is used as the server certificate + if (serverCert==null) + serverCert = sb.toString(); + + n++; + } + this.serverCert = serverCert; + log.debug("AbstractInstallationHelper.initServerCertificate(): Server certificate:\n{}", serverCert); + + // Write server certificate to PEM file (server.pem) + String certFileName = properties.getServerCertFileAtServer(); + if (this.serverCert!=null && StringUtils.isNotEmpty(certFileName)) { + File certFile = new File(certFileName); + Files.writeString(certFile.toPath(), this.serverCert, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + if (! certFile.exists()) + throw new RuntimeException("Server PEM certificate file not found: "+certFile); + log.debug("AbstractInstallationHelper.initServerCertificate(): Server PEM certificate stored in file: {}", certFile); + log.info("Server PEM certificate stored in file: {}", certFile); + } + + } else { + // If HTTPS is disabled + if (StringUtils.isNotEmpty(properties.getServerCertFileAtServer())) { + File certFile = new File(properties.getServerCertFileAtServer()); + if (certFile.exists()) { + log.debug("AbstractInstallationHelper.initServerCertificate(): Removing previous server certificate file"); + if (!certFile.delete()) + throw new RuntimeException("Could not remove previous server certificate file: " + certFile); + } + this.serverCert = null; + } + } + } + + private void initBaguetteClientConfigArchive() throws IOException { + if (StringUtils.isEmpty(properties.getArchiveSourceDir()) || StringUtils.isEmpty(properties.getArchiveFile())) { + log.debug("AbstractInstallationHelper: No baguette client configuration archiving has been configured"); + return; + } + log.info("AbstractInstallationHelper: Building baguette client configuration archive..."); + + // Get archiving settings + String configDirName = properties.getArchiveSourceDir(); + File configDir = new File(configDirName); + log.debug("AbstractInstallationHelper: Baguette client configuration directory: {}", configDir); + if (!configDir.exists()) + throw new FileNotFoundException("Baguette client configuration directory not found: " + configDirName); + + String archiveName = properties.getArchiveFile(); + String archiveDirName = properties.getArchiveDir(); + File archiveDir = new File(archiveDirName); + log.debug("AbstractInstallationHelper: Baguette client configuration archive: {}/{}", archiveDirName, archiveName); + if (!archiveDir.exists()) + throw new FileNotFoundException("Baguette client configuration archive directory not found: " + archiveDirName); + + // Remove previous baguette client configuration archive + File archiveFile = new File(archiveDirName, archiveName); + if (archiveFile.exists()) { + log.debug("AbstractInstallationHelper: Removing previous archive..."); + if (!archiveFile.delete()) + throw new RuntimeException("AbstractInstallationHelper: Failed removing previous archive: " + archiveName); + } + + // Create baguette client configuration archive + Archiver archiver = ArchiverFactory.createArchiver(archiveFile); + String tempFileName = "archive_" + System.currentTimeMillis(); + log.debug("AbstractInstallationHelper: Temp. archive name: {}", tempFileName); + archiveFile = archiver.create(tempFileName, archiveDir, configDir); + log.debug("AbstractInstallationHelper: Archive generated: {}", archiveFile); + if (!archiveFile.getName().equals(archiveName)) { + log.debug("AbstractInstallationHelper: Renaming archive to: {}", archiveName); + if (!archiveFile.renameTo(archiveFile = new File(archiveDir, archiveName))) + throw new RuntimeException("AbstractInstallationHelper: Failed renaming generated archive to: " + archiveName); + } + log.info("AbstractInstallationHelper: Baguette client configuration archive: {}", archiveFile); + + // Base64 encode archive and cache in memory + byte[] archiveBytes = Files.readAllBytes(archiveFile.toPath()); + this.archiveBase64 = Base64.getEncoder().encodeToString(archiveBytes); + log.debug("AbstractInstallationHelper: Archive Base64 encoded: {}", archiveBase64); + } + + private String getResourceAsString(String resourcePath) throws IOException { + InputStream resource = new FileSystemResource(resourcePath).getInputStream(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } + + public Optional> getInstallationInstructionsForOs(NodeRegistryEntry entry) throws IOException { + if (! entry.getBaguetteServer().isServerRunning()) throw new RuntimeException("Baguette Server is not running"); + + List instructionsSets = prepareInstallationInstructionsForOs(entry); + if (instructionsSets==null) { + String nodeOs = entry.getPreregistration().get("operatingSystem"); + log.warn("AbstractInstallationHelper.getInstallationInstructionsForOs(): ERROR: Unknown node OS: {}: node-map={}", nodeOs, entry.getPreregistration()); + return Optional.empty(); + } + + List jsonSets = null; + if (!instructionsSets.isEmpty()) { + // Convert 'instructionsSet' into json string + Gson gson = new Gson(); + jsonSets = instructionsSets.stream().map(instructionsSet -> gson.toJson(instructionsSet, InstructionsSet.class)).collect(Collectors.toList()); + } + log.trace("AbstractInstallationHelper.getInstallationInstructionsForOs(): JSON instruction sets for node: node-map={}\n{}", entry.getPreregistration(), jsonSets); + return Optional.ofNullable(jsonSets); + } + + public List prepareInstallationInstructionsForOs(NodeRegistryEntry entry) throws IOException { + if (! entry.getBaguetteServer().isServerRunning()) throw new RuntimeException("Baguette Server is not running"); + log.trace("AbstractInstallationHelper.prepareInstallationInstructionsForOs(): node-map={}", entry.getPreregistration()); + + String osFamily = entry.getPreregistration().get("operatingSystem"); + List instructionsSetList = null; + if (LINUX_OS_FAMILIES.contains(osFamily.toUpperCase())) + instructionsSetList = prepareInstallationInstructionsForLinux(entry); + else if (WINDOWS_OS_FAMILIES.contains(osFamily.toUpperCase())) + instructionsSetList = prepareInstallationInstructionsForWin(entry); + else + log.warn("AbstractInstallationHelper.prepareInstallationInstructionsForOs(): Unsupported OS family: {}", osFamily); + return instructionsSetList; + } + + protected InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, + Path p, + Path startDir, + String copyToClientDir, + String clientTmpDir, + Map valueMap + ) throws IOException + { + String targetFile = StringUtils.substringAfter(p.toUri().toString(), startDir.toUri().toString()); + if (!targetFile.startsWith("/")) targetFile = "/"+targetFile; + targetFile = copyToClientDir + targetFile; + String contents = new String(Files.readAllBytes(p)); + contents = StringSubstitutor.replace(contents, valueMap); + String tmpFile = clientTmpDir+"/installEMS_"+System.currentTimeMillis(); + instructionsSet + .appendLog(String.format("Copy file from server to temp to client: %s -> %s -> %s", p.toString(), tmpFile, targetFile)); + return _appendCopyInstructions(instructionsSet, targetFile, tmpFile, contents, clientTmpDir); + } + + protected InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, + String targetFile, + String tmpFile, + String contents, + String clientTmpDir + ) throws IOException + { + if (StringUtils.isEmpty(tmpFile)) + tmpFile = clientTmpDir+"/installEMS_"+System.currentTimeMillis(); + instructionsSet + .appendWriteFile(tmpFile, contents, false) + .appendExec("sudo mv " + tmpFile + " " + targetFile) + .appendExec("sudo chmod u+rw,og-rwx " + targetFile); + return instructionsSet; + } + + protected String _prepareUrl(String urlTemplate, String baseUrl) { + return urlTemplate + .replace("%{BASE_URL}%", Optional.ofNullable(baseUrl).orElse("")); + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/InstallationHelper.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/InstallationHelper.java new file mode 100644 index 0000000..420016a --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/InstallationHelper.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.helper; + +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationTask; +import gr.iccs.imu.ems.baguette.client.install.instruction.InstructionsSet; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.translate.TranslationContext; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +public interface InstallationHelper { + Optional> getInstallationInstructionsForOs(NodeRegistryEntry entry) throws IOException; + + List prepareInstallationInstructionsForOs(NodeRegistryEntry entry) throws IOException; + List prepareInstallationInstructionsForWin(NodeRegistryEntry entry); + List prepareInstallationInstructionsForLinux(NodeRegistryEntry entry) throws IOException; + + default ClientInstallationTask createClientInstallationTask(NodeRegistryEntry entry) throws Exception { + return createClientInstallationTask(entry, null); + } + ClientInstallationTask createClientInstallationTask(NodeRegistryEntry entry, TranslationContext translationContext) throws Exception; +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/InstallationHelperFactory.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/InstallationHelperFactory.java new file mode 100644 index 0000000..c3ab4df --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/InstallationHelperFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.helper; + +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +/** + * Installation helper factory + */ +@Slf4j +@Service +public class InstallationHelperFactory implements InitializingBean { + private static InstallationHelperFactory instance; + + public synchronized static InstallationHelperFactory getInstance() { return instance; } + + @Autowired + private ApplicationContext applicationContext; + + @Override + public void afterPropertiesSet() { + InstallationHelperFactory.instance = this; + } + + public InstallationHelper createInstallationHelper(NodeRegistryEntry entry) { + String nodeType = entry.getPreregistration().get("type"); + if ("VM".equalsIgnoreCase(nodeType) || "baremetal".equalsIgnoreCase(nodeType)) { + return createVmInstallationHelper(entry); + } + throw new IllegalArgumentException("Unsupported or missing Node type: "+nodeType); + } + + public InstallationHelper createInstallationHelperBean(String className, NodeRegistryEntry entry) throws ClassNotFoundException { + Class clzz = Class.forName(className); + return (InstallationHelper) applicationContext.getBean(clzz); + } + + public InstallationHelper createInstallationHelperInstance(String className, Map nodeMap) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException + { + Class clzz = Class.forName(className); + return (InstallationHelper) clzz.getDeclaredMethod("getInstance").invoke(null); + } + + private InstallationHelper createVmInstallationHelper(NodeRegistryEntry entry) { + return VmInstallationHelper.getInstance(); + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/VmInstallationHelper.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/VmInstallationHelper.java new file mode 100644 index 0000000..d85264b --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/helper/VmInstallationHelper.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.helper; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationProperties; +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationTask; +import gr.iccs.imu.ems.baguette.client.install.SshConfig; +import gr.iccs.imu.ems.baguette.client.install.instruction.Instruction; +import gr.iccs.imu.ems.baguette.client.install.instruction.InstructionsService; +import gr.iccs.imu.ems.baguette.client.install.instruction.InstructionsSet; +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.CredentialsMap; +import gr.iccs.imu.ems.util.NetUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Baguette Client installation helper + */ +@Slf4j +@Service +public class VmInstallationHelper extends AbstractInstallationHelper { + private final static SimpleDateFormat tsW3C = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + private final static SimpleDateFormat tsUTC = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + private final static SimpleDateFormat tsFile = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss.SSS"); + static { + tsW3C.setTimeZone(TimeZone.getDefault()); + tsUTC.setTimeZone(TimeZone.getTimeZone("UTC")); + tsFile.setTimeZone(TimeZone.getDefault()); + } + + @Autowired + private ResourceLoader resourceLoader; + @Autowired + private ClientInstallationProperties clientInstallationProperties; + + @Override + public ClientInstallationTask createClientInstallationTask(NodeRegistryEntry entry, TranslationContext translationContext) throws IOException { + Map nodeMap = entry.getPreregistration(); + + String baseUrl = nodeMap.get("BASE_URL"); + String clientId = nodeMap.get("CLIENT_ID"); + String ipSetting = nodeMap.get("IP_SETTING"); + + // Extract node identification and type information + String nodeId = nodeMap.get("id"); + String nodeOs = nodeMap.get("operatingSystem"); + String nodeAddress = nodeMap.get("address"); + String nodeType = nodeMap.get("type"); + String nodeName = nodeMap.get("name"); + String nodeProvider = nodeMap.get("provider"); + + if (StringUtils.isBlank(nodeType)) nodeType = "VM"; + + if (StringUtils.isBlank(nodeOs)) throw new IllegalArgumentException("Missing OS information for Node"); + if (StringUtils.isBlank(nodeAddress)) throw new IllegalArgumentException("Missing Address for Node"); + + // Extract node SSH information + int port = (int) Double.parseDouble(Objects.toString(nodeMap.get("ssh.port"), "22")); + if (port<1) port = 22; + String username = nodeMap.get("ssh.username"); + String password = nodeMap.get("ssh.password"); + String privateKey = nodeMap.get("ssh.key"); + String fingerprint = nodeMap.get("ssh.fingerprint"); + + if (port>65535) + throw new IllegalArgumentException("Invalid SSH port for Node: " + port); + if (StringUtils.isBlank(username)) + throw new IllegalArgumentException("Missing SSH username for Node"); + if (StringUtils.isEmpty(password) && StringUtils.isBlank(privateKey)) + throw new IllegalArgumentException("Missing SSH password or private key for Node"); + + // Get EMS client installation instructions for VM node + List instructionsSetList = + prepareInstallationInstructionsForOs(entry); + + // Create Installation Task for VM node + ClientInstallationTask installationTask = ClientInstallationTask.builder() + .id(clientId) + .nodeId(nodeId) + .name(nodeName) + .os(nodeOs) + .address(nodeAddress) + .ssh(SshConfig.builder() + .host(nodeAddress) + .port(port) + .username(username) + .password(password) + .privateKey(privateKey) + .fingerprint(fingerprint) + .build()) + .type(nodeType) + .provider(nodeProvider) + .instructionSets(instructionsSetList) + .nodeRegistryEntry(entry) + .translationContext(translationContext) + .build(); + log.debug("VmInstallationHelper.createClientInstallationTask(): Created client installation task: {}", installationTask); + + return installationTask; + } + + @Override + public List prepareInstallationInstructionsForWin(NodeRegistryEntry entry) { + log.warn("VmInstallationHelper.prepareInstallationInstructionsForWin(): NOT YET IMPLEMENTED"); + throw new IllegalArgumentException("VmInstallationHelper.prepareInstallationInstructionsForWin(): NOT YET IMPLEMENTED"); + } + + @Override + public List prepareInstallationInstructionsForLinux(NodeRegistryEntry entry) throws IOException { + Map nodeMap = entry.getPreregistration(); + BaguetteServer baguette = entry.getBaguetteServer(); + + String baseUrl = StringUtils.removeEnd(nodeMap.get("BASE_URL"), "/"); + String clientId = nodeMap.get("CLIENT_ID"); + String ipSetting = nodeMap.get("IP_SETTING"); + log.debug("VmInstallationHelper.prepareInstallationInstructionsForLinux(): Invoked: base-url={}", baseUrl); + + // Get parameters + log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux(): properties: {}", properties); + String rootCmd = properties.getRootCmd(); + String baseDir = properties.getBaseDir(); + String checkInstallationFile = properties.getCheckInstalledFile(); + + String baseDownloadUrl = _prepareUrl(properties.getDownloadUrl(), baseUrl); + String apiKey = properties.getApiKey(); + String installScriptUrl = _prepareUrl(properties.getInstallScriptUrl(), baseUrl); + String installScriptPath = properties.getInstallScriptFile(); + + String serverCertFile = properties.getServerCertFileAtClient(); + String clientConfArchive = properties.getClientConfArchiveFile(); + + String copyFromServerDir = properties.getCopyFilesFromServerDir(); + String copyToClientDir = properties.getCopyFilesToClientDir(); + + String clientTmpDir = StringUtils.firstNonBlank(properties.getClientTmpDir(), "/tmp"); + + // Create additional keys (with NODE_ prefix) for node map values (as aliases to the already existing keys) + /* + Map additionalKeysMap = nodeMap.entrySet().stream() + .collect(Collectors.toMap( + e -> e.getKey().startsWith("ssh.") + ? "NODE_SSH_" + e.getKey().substring(4).toUpperCase() + : "NODE_" + e.getKey().toUpperCase(), + Map.Entry::getValue, + (v1, v2) -> { + log.warn("VmInstallationHelper.prepareInstallationInstructionsForLinux(): DUPLICATE KEY FOUND: key={}, old-value={}, new-value={}", + k, v1, v2); + return v2; + } + ));*/ + final Map additionalKeysMap = new HashMap<>(); + nodeMap.forEach((k, v) -> { + try { + k = k.startsWith("ssh.") + ? "NODE_SSH_" + k.substring(4).toUpperCase() + : "NODE_" + k.toUpperCase(); + if (additionalKeysMap.containsKey(k)) { + log.warn("VmInstallationHelper.prepareInstallationInstructionsForLinux(): DUPLICATE KEY FOUND: key={}, old-value={}, new-value={}", + k, additionalKeysMap.get(k), v); + } + additionalKeysMap.put(k, v); + } catch (Exception ex) { + log.error("VmInstallationHelper.prepareInstallationInstructionsForLinux(): EXCEPTION in additional keys copy loop: key={}, value={}, additionalKeysMap={}, Exception:\n", + k, v, additionalKeysMap, ex); + } + }); + nodeMap.putAll(additionalKeysMap); + + // Load client config. template and prepare configuration + nodeMap.put("ROOT_CMD", rootCmd!=null ? rootCmd : ""); + nodeMap.put("BAGUETTE_CLIENT_ID", clientId); + nodeMap.put("BAGUETTE_CLIENT_BASE_DIR", baseDir); + nodeMap.put("BAGUETTE_SERVER_ADDRESS", baguette.getConfiguration().getServerAddress()); + nodeMap.put("BAGUETTE_SERVER_HOSTNAME", NetUtil.getHostname()); + nodeMap.put("BAGUETTE_SERVER_PORT", ""+baguette.getConfiguration().getServerPort()); + nodeMap.put("BAGUETTE_SERVER_PUBKEY", baguette.getServerPubkey()); + nodeMap.put("BAGUETTE_SERVER_PUBKEY_FINGERPRINT", baguette.getServerPubkeyFingerprint()); + nodeMap.put("BAGUETTE_SERVER_PUBKEY_ALGORITHM", baguette.getServerPubkeyAlgorithm()); + nodeMap.put("BAGUETTE_SERVER_PUBKEY_FORMAT", baguette.getServerPubkeyFormat()); + CredentialsMap.Entry pair = + baguette.getConfiguration().getCredentials().hasPreferredPair() + ? baguette.getConfiguration().getCredentials().getPreferredPair() + : baguette.getConfiguration().getCredentials().entrySet().iterator().next(); + nodeMap.put("BAGUETTE_SERVER_USERNAME", pair.getKey()); + nodeMap.put("BAGUETTE_SERVER_PASSWORD", pair.getValue()); + + if (StringUtils.isEmpty(ipSetting)) throw new IllegalArgumentException("IP_SETTING must have a value"); + nodeMap.put("IP_SETTING", ipSetting); + + // Misc. installation property values + nodeMap.put("BASE_URL", baseUrl); + nodeMap.put("DOWNLOAD_URL", baseDownloadUrl); + nodeMap.put("API_KEY", apiKey); + nodeMap.put("SERVER_CERT_FILE", serverCertFile); + nodeMap.put("REMOTE_TMP_DIR", clientTmpDir); + + Date ts = new Date(); + nodeMap.put("TIMESTAMP", Long.toString(ts.getTime())); + nodeMap.put("TIMESTAMP-W3C", tsW3C.format(ts)); + nodeMap.put("TIMESTAMP-UTC", tsUTC.format(ts)); + nodeMap.put("TIMESTAMP-FILE", tsFile.format(ts)); + + nodeMap.putAll(clientInstallationProperties.getParameters()); + nodeMap.put("EMS_PUBLIC_DIR", System.getProperty("PUBLIC_DIR", System.getenv("PUBLIC_DIR"))); + log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: value-map: {}", nodeMap); + +/* // Clear EMS server certificate (PEM) file, if not secure + if (!isServerSecure) { + serverCertFile = ""; + } + + // Copy files from server to Baguette Client + if (StringUtils.isNotEmpty(copyFromServerDir) && StringUtils.isNotEmpty(copyToClientDir)) { + Path startDir = Paths.get(copyFromServerDir).toAbsolutePath(); + try (Stream stream = Files.walk(startDir, Integer.MAX_VALUE)) { + List paths = stream + .filter(Files::isRegularFile) + .map(Path::toAbsolutePath) + .sorted() + .collect(Collectors.toList()); + for (Path p : paths) { + _appendCopyInstructions(instructionSets, p, startDir, copyToClientDir, clientTmpDir, valueMap); + } + } + }*/ + + List instructionsSetList = new ArrayList<>(); + + try { + // Read installation instructions from JSON file + List instructionSetFileList = null; + if (nodeMap.containsKey("instruction-files")) { + instructionSetFileList = Arrays.stream(nodeMap.getOrDefault("instruction-files", "").split(",")) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); + if (instructionSetFileList.isEmpty()) + log.warn("VmInstallationHelper.prepareInstallationInstructionsForLinux: Context map contains 'instruction-files' entry with no contents"); + } else { + instructionSetFileList = properties.getInstructions().get("LINUX"); + } + for (String instructionSetFile : instructionSetFileList) { + // Load instructions set from file + log.debug("VmInstallationHelper.prepareInstallationInstructionsForLinux: Installation instructions file for LINUX: {}", instructionSetFile); + InstructionsSet instructionsSet = InstructionsService.getInstance().loadInstructionsFile(instructionSetFile); + log.debug("VmInstallationHelper.prepareInstallationInstructionsForLinux: Instructions set loaded from file: {}\n{}", instructionSetFile, instructionsSet); + + // Pretty print instructionsSet JSON + if (log.isTraceEnabled()) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + StringWriter stringWriter = new StringWriter(); + try (PrintWriter writer = new PrintWriter(stringWriter)) { + gson.toJson(instructionsSet, writer); + } + log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: Installation instructions for LINUX: json:\n{}", stringWriter); + } + + instructionsSetList.add(instructionsSet); + } + + return instructionsSetList; + } catch (Exception ex) { + log.error("VmInstallationHelper.prepareInstallationInstructionsForLinux: Exception while reading Installation instructions for LINUX: ", ex); + throw ex; + } + } + + private InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, + Path path, + Path localBaseDir, + String remoteTargetDir, + Map valueMap + ) throws IOException + { + String targetFile = StringUtils.substringAfter(path.toUri().toString(), localBaseDir.toUri().toString()); + if (!targetFile.startsWith("/")) targetFile = "/"+targetFile; + targetFile = remoteTargetDir + targetFile; + String contents = new String(Files.readAllBytes(path)); + contents = StringSubstitutor.replace(contents, valueMap); + String description = String.format("Copy file from server to temp to client: %s -> %s", path.toString(), targetFile); + return _appendCopyInstructions(instructionsSet, targetFile, description, contents); + } + + private InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, + String targetFile, + String description, + String contents) + { + instructionsSet + .appendInstruction(Instruction.createWriteFile(targetFile, contents, false).description(description)) + .appendExec("sudo chmod u+rw,og-rwx " + targetFile); + return instructionsSet; + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/AbstractInstructionsBase.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/AbstractInstructionsBase.java new file mode 100644 index 0000000..16ba9a4 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/AbstractInstructionsBase.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.instruction; + +import lombok.Data; + +@Data +public abstract class AbstractInstructionsBase { + private String condition; + private boolean stopOnConditionFail; +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/INSTRUCTION_RESULT.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/INSTRUCTION_RESULT.java new file mode 100644 index 0000000..c6b5624 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/INSTRUCTION_RESULT.java @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.instruction; + +public enum INSTRUCTION_RESULT { SUCCESS, FAIL, EXIT } diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/INSTRUCTION_TYPE.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/INSTRUCTION_TYPE.java new file mode 100644 index 0000000..90ee7f5 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/INSTRUCTION_TYPE.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.instruction; + +public enum INSTRUCTION_TYPE { + LOG, CHECK, CMD, SHELL, FILE, COPY, UPLOAD, DOWNLOAD, + SET_VARS, UNSET_VARS, PRINT_VARS, EXIT, EXIT_SET +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/Instruction.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/Instruction.java new file mode 100644 index 0000000..0645255 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/Instruction.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.instruction; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.util.Map; +import java.util.regex.Pattern; + +@Data +@Accessors(chain = true, fluent = true) +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Getter(onMethod = @__(@JsonProperty)) +public class Instruction extends AbstractInstructionsBase { + private INSTRUCTION_TYPE taskType; + private String description; + private String message; + private String command; + private String fileName; + private String localFileName; + private String contents; + private boolean executable; + private int exitCode; + private boolean match; + private long executionTimeout; + private int retries; + + private Map patterns; + private Map variables; + + // Fluent API addition + public Instruction pattern(String varName, Pattern pattern) { + this.patterns.put(varName, pattern); + return this; + } + + // Creators API + public static Instruction createLog(@NotNull String message) { + return Instruction.builder() + .taskType(INSTRUCTION_TYPE.LOG) + .command(message) + .build(); + } + + public static Instruction createShellCommand(@NotNull String command) { + return Instruction.builder() + .taskType(INSTRUCTION_TYPE.CMD) + .command(command) + .build(); + } + + public static Instruction createWriteFile(@NotNull String file, String contents, boolean executable) { + return Instruction.builder() + .taskType(INSTRUCTION_TYPE.FILE) + .fileName(file) + .contents(contents==null ? "" : contents) + .executable(executable) + .build(); + } + + public static Instruction createUploadFile(@NotNull String localFile, @NotNull String remoteFile) { + return Instruction.builder() + .taskType(INSTRUCTION_TYPE.COPY) + .fileName(remoteFile) + .localFileName(localFile) + .build(); + } + + public static Instruction createDownloadFile(@NotNull String remoteFile, @NotNull String localFile) { + return Instruction.builder() + .taskType(INSTRUCTION_TYPE.DOWNLOAD) + .fileName(remoteFile) + .localFileName(localFile) + .build(); + } + + public static Instruction createCheck(@NotNull String command, @NotNull int exitCode, boolean match, String message) { + return Instruction.builder() + .taskType(INSTRUCTION_TYPE.CHECK) + .command(command) + .exitCode(exitCode) + .match(match) + .contents(message) + .build(); + } +} \ No newline at end of file diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/InstructionsService.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/InstructionsService.java new file mode 100644 index 0000000..23d64ae --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/InstructionsService.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.instruction; + +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.stereotype.Service; +import org.springframework.util.FileCopyUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class InstructionsService implements EnvironmentAware { + private Environment environment; + private final ResourceLoader resourceLoader; + private static InstructionsService INSTANCE; + + public static InstructionsService getInstance() { + if (INSTANCE==null) throw new IllegalStateException("InstructionsService singleton instance has not yet been initialized"); + return INSTANCE; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + INSTANCE = this; + } + + public boolean checkCondition(@NonNull AbstractInstructionsBase i, Map valueMap) { + log.trace("InstructionsService: checkCondition: condition={}, value-map={}", i.getCondition(), valueMap); + String condition = i.getCondition(); + if (StringUtils.isBlank(condition)) return true; + String conditionResolved = processPlaceholders(condition, valueMap); + log.trace("InstructionsService: checkCondition: Expression after placeholder resolution: {}", conditionResolved); + final ExpressionParser parser = new SpelExpressionParser(); + Object result = parser.parseExpression(conditionResolved).getValue(); + log.trace("InstructionsService: checkCondition: Expression result: {}", result); + if (result==null) + throw new IllegalArgumentException("Condition evaluation returned null: " + condition); + if (result instanceof Boolean) + return (Boolean)result; + throw new IllegalArgumentException("Condition evaluation returned a non-boolean value: " + result + ", condition: " + condition+", resolved condition: "+ conditionResolved); + } + + public Instruction resolvePlaceholders(Instruction instruction, Map valueMap) { + return instruction.toBuilder() + .description(processPlaceholders(instruction.description(), valueMap)) + .message(processPlaceholders(instruction.message(), valueMap)) + .command(processPlaceholders(instruction.command(), valueMap)) + .fileName(processPlaceholders(instruction.fileName(), valueMap)) + .localFileName(processPlaceholders(instruction.localFileName(), valueMap)) + .contents(processPlaceholders(instruction.contents(), valueMap)) + .build(); + } + + public String processPlaceholders(String s, Map valueMap) { + if (StringUtils.isBlank(s)) return s; + s = StringSubstitutor.replace(s, valueMap); + s = environment.resolvePlaceholders(s); + //s = environment.resolveRequiredPlaceholders(s); + s = s.replace('\\', '/'); + return s; + } + + public InstructionsSet loadInstructionsFile(@NonNull String fileName) throws IOException { + if (StringUtils.isBlank(fileName)) + throw new IllegalArgumentException("File name is blank"); + fileName = fileName.trim(); + + // Get file type from file extension + String ext = null; + int i = fileName.lastIndexOf('.'); + if (i > 0) { + ext = fileName.substring(i+1); + if (ext.contains("/") || ext.contains("\\")) ext = null; + } + if (ext==null) + throw new IllegalArgumentException("Unknown file type: "+fileName); + + // Process instructions file based on its type + try { + if ("json".equalsIgnoreCase(ext)) { + // Load instructions set from JSON file + return _loadFromJsonFile(fileName); + } else if ("yml".equalsIgnoreCase(ext) || "yaml".equalsIgnoreCase(ext)) { + // Load instructions set from YAML file + return _loadFromYamlFile(fileName); + } else if ("js".equalsIgnoreCase(ext)) { + // Just return an instruction set with the file name set + InstructionsSet is = new InstructionsSet(); + is.setFileName(fileName); + return is; + } + } catch (IOException e) { + log.error("Exception thrown while processing instructions set file: {}", fileName); + throw new IOException(fileName+": "+e.getMessage(), e); + } + throw new IllegalArgumentException("Unsupported file type: "+fileName); + } + + private InstructionsSet _loadFromJsonFile(String jsonFile) throws IOException { + log.debug("InstructionsService: Loading instructions from JSON file: {}", jsonFile); + byte[] bdata = FileCopyUtils.copyToByteArray(resourceLoader.getResource(jsonFile).getInputStream()); + String jsonStr = new String(bdata, StandardCharsets.UTF_8); + log.trace("InstructionsService: JSON instructions file contents: \n{}", jsonStr); + + // Create InstructionsSet object from JSON + ObjectMapper mapper = new ObjectMapper(); + InstructionsSet instructionsSet = mapper.readerFor(InstructionsSet.class) + .with(JsonReadFeature.ALLOW_JAVA_COMMENTS) + .readValue(jsonStr); + instructionsSet.setFileName(jsonFile); + log.trace("InstructionsService: Installation instructions loaded from JSON file: {}\n{}", jsonFile, instructionsSet); + + return instructionsSet; + } + + private InstructionsSet _loadFromYamlFile(String yamlFile) throws IOException { + log.debug("InstructionsService: Loading instructions from YAML file: {}", yamlFile); + byte[] bdata = FileCopyUtils.copyToByteArray(resourceLoader.getResource(yamlFile).getInputStream()); + String yamlStr = new String(bdata, StandardCharsets.UTF_8); + log.trace("InstructionsService: YAML instructions file contents: \n{}", yamlStr); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + InstructionsSet instructionsSet = + mapper.readValue(yamlStr, InstructionsSet.class); + instructionsSet.setFileName(yamlFile); + log.trace("InstructionsService: Installation instructions loaded from YAML file: {}\n{}", yamlFile, instructionsSet); + + return instructionsSet; + } +} \ No newline at end of file diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/InstructionsSet.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/InstructionsSet.java new file mode 100644 index 0000000..d0b722d --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/instruction/InstructionsSet.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.instruction; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.*; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class InstructionsSet extends AbstractInstructionsBase { + private String os; + private String description; + private String fileName; + private List instructions = new ArrayList<>(); + + public List getInstructions() { + return Collections.unmodifiableList(instructions); + } + + public void setInstructions(List ni) { + instructions = new ArrayList<>(ni); + } + + public InstructionsSet appendInstruction(Instruction i) { + instructions.add(i); + return this; + } + + public InstructionsSet appendLog(String message) { + instructions.add(Instruction.createLog(message)); + return this; + } + + public InstructionsSet appendExec(String command) { + instructions.add(Instruction.createShellCommand(command)); + return this; + } + + public InstructionsSet appendWriteFile(String file, String contents, boolean executable) { + instructions.add(Instruction.createWriteFile(file, contents, executable)); + return this; + } + + public InstructionsSet appendUploadFile(String localFile, String remoteFile) { + instructions.add(Instruction.createUploadFile(localFile, remoteFile)); + return this; + } + + public InstructionsSet appendDownloadFile(String remoteFile, String localFile) { + instructions.add(Instruction.createDownloadFile(remoteFile, localFile)); + return this; + } + + public InstructionsSet appendCheck(String command, int exitCode, boolean match, String message) { + instructions.add(Instruction.createCheck(command, exitCode, match, message)); + return this; + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/plugin/AllowedTopicsProcessorPlugin.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/plugin/AllowedTopicsProcessorPlugin.java new file mode 100644 index 0000000..b7d3862 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/plugin/AllowedTopicsProcessorPlugin.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.plugin; + +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationTask; +import gr.iccs.imu.ems.baguette.client.install.InstallationContextProcessorPlugin; +import gr.iccs.imu.ems.translate.model.Monitor; +import gr.iccs.imu.ems.util.EmsConstant; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * Installation context processor plugin for generating 'allowed-topics' setting + * used in baguette-client[.yml/.properties] config. file. + * It set the 'COLLECTOR_ALLOWED_TOPICS' variable in pre-registration context. + */ +@Slf4j +@Data +@Service +public class AllowedTopicsProcessorPlugin implements InstallationContextProcessorPlugin { + @Override + public void processBeforeInstallation(ClientInstallationTask task, long taskCounter) { + log.debug("AllowedTopicsProcessorPlugin: Task #{}: processBeforeInstallation: BEGIN", taskCounter); + log.trace("AllowedTopicsProcessorPlugin: Task #{}: processBeforeInstallation: BEGIN: task={}", taskCounter, task); + + StringBuilder sbAllowedTopics = new StringBuilder(); + Set addedTopicsSet = new HashSet<>(); + + boolean first = true; + for (Monitor monitor : task.getTranslationContext().getMON()) { + try { + log.trace("AllowedTopicsProcessorPlugin: Task #{}: Processing monitor: {}", taskCounter, monitor); + + String metricName = monitor.getMetric(); + if (!addedTopicsSet.contains(metricName)) { + if (first) first = false; + else sbAllowedTopics.append(", "); + + sbAllowedTopics.append(metricName); + addedTopicsSet.add(metricName); + } + + // Get sensor configuration (as a list of KeyValuePair's) + Map sensorConfig = null; + if (monitor.getSensor().isPullSensor()) { + // Pull Sensor + sensorConfig = monitor.getSensor().pullSensor().getConfiguration(); + } else { + // Push Sensor + sensorConfig = monitor.getSensor().pushSensor().getAdditionalProperties(); + } + + // Process Destination aliases, if specified in configuration + if (sensorConfig!=null) { + String k = sensorConfig.keySet().stream() + .filter(key -> StrUtil.compareNormalized(key, EmsConstant.COLLECTOR_DESTINATION_ALIASES)) + .findAny().orElse(null); + String aliases = (k!=null) ? sensorConfig.get(k) : null; + + if (StringUtils.isNotBlank(aliases)) { + for (String alias : aliases.trim().split(EmsConstant.COLLECTOR_DESTINATION_ALIASES_DELIMITERS)) { + if (!(alias=alias.trim()).isEmpty()) { + if (!alias.equals(metricName)) { + sbAllowedTopics.append(", "); + sbAllowedTopics.append(alias).append(":").append(metricName); + } + } + } + } + } + + log.trace("AllowedTopicsProcessorPlugin: Task #{}: MONITOR: metric={}, allowed-topics={}", + taskCounter, metricName, sbAllowedTopics); + + } catch (Exception e) { + log.error("AllowedTopicsProcessorPlugin: Task #{}: EXCEPTION while processing monitor. Skipping it: {}\n", + taskCounter, monitor, e); + } + } + + String allowedTopics = sbAllowedTopics.toString(); + log.debug("AllowedTopicsProcessorPlugin: Task #{}: Allowed-Topics configuration for collectors: \n{}", taskCounter, allowedTopics); + + task.getNodeRegistryEntry().getPreregistration().put(EmsConstant.COLLECTOR_ALLOWED_TOPICS_VAR, allowedTopics); + log.debug("AllowedTopicsProcessorPlugin: Task #{}: processBeforeInstallation: END", taskCounter); + } + + @Override + public void processAfterInstallation(ClientInstallationTask task, long taskCounter, boolean success) { + log.debug("AllowedTopicsProcessorPlugin: Task #{}: processAfterInstallation: success={}", taskCounter, success); + log.trace("AllowedTopicsProcessorPlugin: Task #{}: processAfterInstallation: success={}, task={}", taskCounter, success, task); + } + + @Override + public void start() { + log.debug("AllowedTopicsProcessorPlugin: start()"); + } + + @Override + public void stop() { + log.debug("AllowedTopicsProcessorPlugin: stop()"); + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/plugin/PrometheusProcessorPlugin.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/plugin/PrometheusProcessorPlugin.java new file mode 100644 index 0000000..49aac28 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/install/plugin/PrometheusProcessorPlugin.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.install.plugin; + +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationTask; +import gr.iccs.imu.ems.baguette.client.install.InstallationContextProcessorPlugin; +import gr.iccs.imu.ems.translate.model.Interval; +import gr.iccs.imu.ems.translate.model.Monitor; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Installation context processor plugin for generating Netdata configuration for collecting metrics from prometheus exporters + */ +@Slf4j +@Data +@Service +public class PrometheusProcessorPlugin implements InstallationContextProcessorPlugin { + public final static String SENSOR_TYPE_KEY = "pull.sensor.type"; + public final static String SENSOR_TYPE_VALUE = "prometheus"; + public final static String NETDATA_PROMETHEUS_JOB_NAME = "pull.prometheus.job.name"; + public final static String NETDATA_PROMETHEUS_ENDPOINT = "pull.prometheus.endpoint"; + public final static String NETDATA_PROMETHEUS_AUTODETECTION = "pull.prometheus.autodetection"; + public final static String NETDATA_PROMETHEUS_PRIORITY = "pull.prometheus.priority"; + public final static String NETDATA_PROMETHEUS_CONFIGURATION_VAR = "NETDATA_PROMETHEUS_CONF"; + public final static long DEFAULT_PRIORITY = 70000; + + @Override + public void processBeforeInstallation(ClientInstallationTask task, long taskCounter) { + log.debug("PrometheusProcessorPlugin: Task #{}: processBeforeInstallation: BEGIN", taskCounter); + log.trace("PrometheusProcessorPlugin: Task #{}: processBeforeInstallation: BEGIN: task={}", taskCounter, task); + + StringBuilder prometheusConf = new StringBuilder("# Generated on: ").append(new Date()).append("\n\n"); + int headerLength = prometheusConf.length(); + + long minCollectionInterval = Long.MAX_VALUE; + long minAutodetectionInterval = Long.MAX_VALUE; + long minPriority = DEFAULT_PRIORITY; + boolean found = false; + + prometheusConf.append("\njobs:\n"); + for (Monitor monitor : task.getTranslationContext().getMON()) { + try { + log.trace("PrometheusProcessorPlugin: Task #{}: Processing monitor: {}", taskCounter, monitor); + String componentName = monitor.getComponent(); + String metricName = monitor.getMetric(); + + log.trace("PrometheusProcessorPlugin: Task #{}: MONITOR: component={}, metric={}", taskCounter, componentName, metricName); + if (monitor.getSensor().isPullSensor()) { + if (monitor.getSensor().pullSensor().getConfiguration()!=null) { + Map config = monitor.getSensor().pullSensor().getConfiguration(); + log.trace("PrometheusProcessorPlugin: Task #{}: MONITOR with PULL SENSOR: config: {}", taskCounter, config); + + // Get Prometheus related settings + String sensorType = StrUtil.getWithNormalized(config, SENSOR_TYPE_KEY, SENSOR_TYPE_VALUE); + String prometheusJobName = StrUtil.getWithNormalized(config, NETDATA_PROMETHEUS_JOB_NAME); + String prometheusEndpoint = StrUtil.getWithNormalized(config, NETDATA_PROMETHEUS_ENDPOINT); + log.trace("PrometheusProcessorPlugin: Task #{}: Prometheus Job settings: type={}, name={}, endpoint={}", + taskCounter, sensorType, prometheusJobName, prometheusEndpoint); + if (SENSOR_TYPE_VALUE.equals(sensorType)) { + if (StringUtils.isNotBlank(prometheusJobName) && StringUtils.isNotBlank(prometheusEndpoint)) { + prometheusConf.append(" - name: '").append(prometheusJobName).append("'\n"); + prometheusConf.append(" url: '").append(prometheusEndpoint).append("'\n"); + log.trace("PrometheusProcessorPlugin: Task #{}: Extracted Prometheus config: metricName={}, endpoint={}", + taskCounter, prometheusJobName, prometheusEndpoint); + found = true; + + // Get monitor interval + Interval interval = monitor.getSensor().pullSensor().getInterval(); + if (interval != null) { + int period = interval.getPeriod(); + TimeUnit unit = TimeUnit.SECONDS; + if (interval.getUnit() != null) { + unit = TimeUnit.valueOf( interval.getUnit().name() ); + } + long periodInSeconds = TimeUnit.SECONDS.convert(period, unit); + if (periodInSeconds > 0) + minCollectionInterval = Math.min(minCollectionInterval, periodInSeconds); + } + + // Get autodetection interval + String autodetectionStr = StrUtil.getWithNormalized(config, NETDATA_PROMETHEUS_AUTODETECTION); + int autodetectionInSeconds = StrUtil.strToInt(autodetectionStr, 0, i -> i >= 0, false, null); + if (autodetectionInSeconds > 0) + minAutodetectionInterval = Math.min(minAutodetectionInterval, autodetectionInSeconds); + + // Get priority + String priorityStr = StrUtil.getWithNormalized(config, NETDATA_PROMETHEUS_PRIORITY); + int priority = StrUtil.strToInt(priorityStr, (int)DEFAULT_PRIORITY, i -> i >= 0, false, null); + if (priority >= 0) + minPriority = Math.min(minPriority, priority); + } + } else { + log.debug("PrometheusProcessorPlugin: Task #{}: Sensor type is not Prometheus: {}", taskCounter, sensorType); + } + } + } + + } catch (Exception e) { + log.error("PrometheusProcessorPlugin: Task #{}: EXCEPTION while processing monitor. Skipping it: {}\n", taskCounter, monitor, e); + } + } + log.debug("PrometheusProcessorPlugin: Task #{}: Netdata Prometheus configuration: \n{}", taskCounter, prometheusConf); + log.debug("PrometheusProcessorPlugin: Task #{}: Netdata Prometheus: found={}, collection-interval={}, autodetection={}, priority={}", + taskCounter, found, minCollectionInterval, minAutodetectionInterval, minPriority); + + if (!found) { + task.getNodeRegistryEntry().getPreregistration().put(NETDATA_PROMETHEUS_CONFIGURATION_VAR, ""); + log.debug("PrometheusProcessorPlugin: Task #{}: processBeforeInstallation: END: no prometheus.conf update", taskCounter); + } else + { + if (minCollectionInterval < Long.MAX_VALUE) + prometheusConf.insert(headerLength, "update_every: " + minCollectionInterval + "\n"); + if (minAutodetectionInterval < Long.MAX_VALUE) + prometheusConf.insert(headerLength, "autodetection_retry: " + minAutodetectionInterval + "\n"); + if (minPriority != DEFAULT_PRIORITY) + prometheusConf.insert(headerLength, "priority: " + minPriority + "\n"); + + task.getNodeRegistryEntry().getPreregistration().put(NETDATA_PROMETHEUS_CONFIGURATION_VAR, prometheusConf.toString()); + log.debug("PrometheusProcessorPlugin: Task #{}: processBeforeInstallation: END", taskCounter); + } + } + + @Override + public void processAfterInstallation(ClientInstallationTask task, long taskCounter, boolean success) { + log.debug("PrometheusProcessorPlugin: Task #{}: processAfterInstallation: success={}", taskCounter, success); + log.trace("PrometheusProcessorPlugin: Task #{}: processAfterInstallation: success={}, task={}", taskCounter, success, task); + } + + @Override + public void start() { + log.debug("PrometheusProcessorPlugin: start()"); + } + + @Override + public void stop() { + log.debug("PrometheusProcessorPlugin: stop()"); + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/ClientRecoveryPlugin.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/ClientRecoveryPlugin.java new file mode 100644 index 0000000..25b2232 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/ClientRecoveryPlugin.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.selfhealing; + +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationProperties; +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationTask; +import gr.iccs.imu.ems.baguette.client.install.SshClientInstaller; +import gr.iccs.imu.ems.baguette.client.install.helper.InstallationHelperFactory; +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistry; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.common.selfhealing.SelfHealingManager; +import gr.iccs.imu.ems.util.EmsConstant; +import gr.iccs.imu.ems.util.EventBus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.HashMap; +import java.util.concurrent.ScheduledFuture; + +@Slf4j +@Service +@ConditionalOnProperty(name = "enabled", prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "self.healing", havingValue = "true", matchIfMissing = true) +@RequiredArgsConstructor +public class ClientRecoveryPlugin implements InitializingBean, EventBus.EventConsumer { + private final EventBus eventBus; + private final NodeRegistry nodeRegistry; + private final TaskScheduler taskScheduler; + private final ClientInstallationProperties clientInstallationProperties; + private final ServerSelfHealingProperties selfHealingProperties; + private final BaguetteServer baguetteServer; + + private final HashMap> pendingTasks = new HashMap<>(); + + private long clientRecoveryDelay; + private String recoveryInstructionsFile; + + private final static String CLIENT_EXIT_TOPIC = "BAGUETTE_SERVER_CLIENT_EXITED"; + private final static String CLIENT_REGISTERED_TOPIC = "BAGUETTE_SERVER_CLIENT_REGISTERED"; + + @Override + public void afterPropertiesSet() throws Exception { + clientRecoveryDelay = selfHealingProperties.getRecovery().getDelay(); + recoveryInstructionsFile = selfHealingProperties.getRecovery().getFile().getOrDefault("baguette", ""); + log.debug("ClientRecoveryPlugin: recovery-delay={}, recovery-instructions-file (for baguette)={}", clientRecoveryDelay, recoveryInstructionsFile); + + eventBus.subscribe(CLIENT_EXIT_TOPIC, this); + log.debug("ClientRecoveryPlugin: Subscribed for BAGUETTE_SERVER_CLIENT_EXITED events"); + eventBus.subscribe(CLIENT_REGISTERED_TOPIC, this); + log.debug("ClientRecoveryPlugin: Subscribed for BAGUETTE_SERVER_CLIENT_REGISTERED events"); + + log.trace("ClientRecoveryPlugin: clientInstallationProperties: {}", clientInstallationProperties); + log.trace("ClientRecoveryPlugin: baguetteServer: {}", baguetteServer); + + log.debug("ClientRecoveryPlugin: Recovery Delay: {}", clientRecoveryDelay); + log.debug("ClientRecoveryPlugin: Recovery Instructions File: {}", recoveryInstructionsFile); + } + + @Override + public void onMessage(String topic, Object message, Object sender) { + log.debug("ClientRecoveryPlugin: onMessage(): BEGIN: topic={}, message={}, sender={}", topic, message, sender); + + // Check if Self-Healing is enabled + if (! baguetteServer.getSelfHealingManager().isEnabled()) { + log.debug("ClientRecoveryPlugin: onMessage(): Self-Healing manager is disabled: message={}, sender={}", message, sender); + return; + } + + // Only process messages of ClientShellCommand type are accepted (sent by CSC instances) + if (! (message instanceof ClientShellCommand)) { + log.warn("ClientRecoveryPlugin: onMessage(): Message is not a {} object. Will ignore it.", ClientShellCommand.class.getSimpleName()); + return; + } + + // Get NodeRegistryEntry from ClientShellCommand passed with event + ClientShellCommand csc = (ClientShellCommand)message; + String clientId = csc.getId(); + String address = csc.getClientIpAddress(); + log.debug("ClientRecoveryPlugin: onMessage(): client-id={}, client-address={}", clientId, address); + + NodeRegistryEntry nodeInfo = csc.getNodeRegistryEntry(); //or = nodeRegistry.getNodeByAddress(address); + log.debug("ClientRecoveryPlugin: onMessage(): client-node-info={}", nodeInfo); + log.trace("ClientRecoveryPlugin: onMessage(): node-registry.node-addresses={}", nodeRegistry.getNodeAddresses()); + log.trace("ClientRecoveryPlugin: onMessage(): node-registry.nodes={}", nodeRegistry.getNodes()); + + // Check if node is monitored by Self-Healing manager + if (! baguetteServer.getSelfHealingManager().isMonitored(nodeInfo)) { + log.warn("ClientRecoveryPlugin: processExitEvent(): Node is not monitored by Self-Healing manager: client-id={}, client-address={}", clientId, address); + return; + } + + // Process event + if (CLIENT_EXIT_TOPIC.equals(topic)) { + log.debug("ClientRecoveryPlugin: onMessage(): CLIENT EXITED: message={}", message); + processExitEvent(nodeInfo); + } + if (CLIENT_REGISTERED_TOPIC.equals(topic)) { + log.debug("ClientRecoveryPlugin: onMessage(): CLIENT REGISTERED_TOPIC: message={}", message); + processRegisteredEvent(nodeInfo); + } + } + + private void processExitEvent(NodeRegistryEntry nodeInfo) { + log.debug("ClientRecoveryPlugin: processExitEvent(): BEGIN: client-id={}, client-address={}", nodeInfo.getClientId(), nodeInfo.getIpAddress()); + + // Set node state to DOWN + baguetteServer.getSelfHealingManager().setNodeSelfHealingState(nodeInfo, SelfHealingManager.NODE_STATE.DOWN); + + // Schedule a recovery task for node + ScheduledFuture future = taskScheduler.schedule(() -> { + try { + // Set node state to RECOVERING + baguetteServer.getSelfHealingManager().setNodeSelfHealingState(nodeInfo, SelfHealingManager.NODE_STATE.RECOVERING); + // Run recovery task + runClientRecovery(nodeInfo); + } catch (Exception e) { + log.error("ClientRecoveryPlugin: processExitEvent(): EXCEPTION: while recovering node: node-info={} -- Exception: ", nodeInfo, e); + } + }, Instant.now().plusMillis(clientRecoveryDelay)); + + // Register the recovery task's future in pending list + ScheduledFuture old = pendingTasks.put(nodeInfo, future); + log.info("ClientRecoveryPlugin: processExitEvent(): Added recovery task in the queue: client-id={}, client-address={}", nodeInfo.getClientId(), nodeInfo.getIpAddress()); + + // Cancel any previous recovery task (for the node) that is still pending + if (old!=null && ! old.isDone() && ! old.isCancelled()) { + log.warn("ClientRecoveryPlugin: processExitEvent(): Cancelled previous recovery task: client-id={}, client-address={}", nodeInfo.getClientId(), nodeInfo.getIpAddress()); + old.cancel(false); + } + } + + private void processRegisteredEvent(NodeRegistryEntry nodeInfo) { + log.debug("ClientRecoveryPlugin: processRegisteredEvent(): BEGIN: client-id={}, client-address={}", nodeInfo.getClientId(), nodeInfo.getIpAddress()); + + // Cancel any pending recovery task (for the node) + ScheduledFuture future = pendingTasks.remove(nodeInfo); + if (future!=null && ! future.isDone() && ! future.isCancelled()) { + log.warn("ClientRecoveryPlugin: processRegisteredEvent(): Cancelled recovery task: client-id={}, client-address={}", nodeInfo.getClientId(), nodeInfo.getIpAddress()); + future.cancel(false); + } + + // Set node state to UP + baguetteServer.getSelfHealingManager().setNodeSelfHealingState(nodeInfo, SelfHealingManager.NODE_STATE.UP); + } + + public void runClientRecovery(NodeRegistryEntry entry) throws Exception { + log.debug("ClientRecoveryPlugin: runClientRecovery(): node-info={}", entry); + if (entry==null) return; + + log.trace("ClientRecoveryPlugin: runClientRecovery(): recoveryInstructionsFile={}", recoveryInstructionsFile); + entry.getPreregistration().put("instruction-files", recoveryInstructionsFile); + + ClientInstallationTask task = InstallationHelperFactory.getInstance() + .createInstallationHelper(entry) + .createClientInstallationTask(entry); + log.debug("ClientRecoveryPlugin: runClientRecovery(): Client recovery task: {}", task); + SshClientInstaller installer = SshClientInstaller.builder() + .task(task) + .properties(clientInstallationProperties) + .build(); + + log.info("ClientRecoveryPlugin: runClientRecovery(): Starting client recovery: client-id={}, client-address={}", entry.getClientId(), entry.getIpAddress()); + log.debug("ClientRecoveryPlugin: runClientRecovery(): Starting client recovery: node-info={}", entry); + boolean result = installer.execute(); + pendingTasks.remove(entry); + log.info("ClientRecoveryPlugin: runClientRecovery(): Client recovery completed: result={}, client-id={}, client-address={}", result, entry.getClientId(), entry.getIpAddress()); + log.debug("ClientRecoveryPlugin: runClientRecovery(): Client recovery completed: result={}, node-info={}", result, entry); + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/SelfHealingManagerImpl.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/SelfHealingManagerImpl.java new file mode 100644 index 0000000..05c2db1 --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/SelfHealingManagerImpl.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.selfhealing; + +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationProperties; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.common.recovery.RecoveryContext; +import gr.iccs.imu.ems.common.selfhealing.SelfHealingManager; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +@Data +@Service +public class SelfHealingManagerImpl implements SelfHealingManager, InitializingBean { + private final ClientInstallationProperties clientInstallationProperties; + private final ServerSelfHealingProperties properties; + private final RecoveryContext recoveryContext; + + private boolean enabled; + private MODE mode; + private Map nodes = new LinkedHashMap<>(); + private Map nodeStates = new LinkedHashMap<>(); + private Map nodeStateTexts = new LinkedHashMap<>(); + + @Override + public void afterPropertiesSet() throws Exception { + log.info("Self-Healing Manager initialized"); + setEnabled( properties.isEnabled() ); + setMode( properties.getMode() ); + + // Initialize recovery context + recoveryContext.initialize(clientInstallationProperties, properties); + log.warn("Recovery context: {}", recoveryContext); + } + + private void check() { + if (!enabled) throw new IllegalStateException("SelfHealingManager is not enabled"); + } + + @Override + public Collection getNodes() { + check(); + return Collections.unmodifiableCollection(nodes.values()); + } + + @Override + public boolean containsNode(@NonNull NodeRegistryEntry node) { + check(); + return nodes.containsKey(node.getIpAddress()); + } + + @Override + public boolean containsAny(@NonNull Collection nodes) { + check(); + return Collections.disjoint(this.nodes.values(), nodes); + } + + @Override + public boolean isMonitored(@NonNull NodeRegistryEntry node) { + check(); + return mode==MODE.ALL || + mode==MODE.INCLUDED && containsNode(node) || + mode==MODE.EXCLUDED && ! containsNode(node); + } + + @Override + public void addNode(@NonNull NodeRegistryEntry node) { + check(); + nodes.put(node.getIpAddress(), node); + } + + @Override + public void addAllNodes(@NonNull Collection nodes) { + check(); + this.nodes.putAll(nodes.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(NodeRegistryEntry::getIpAddress, Function.identity()))); + } + + @Override + public void removeNode(@NonNull NodeRegistryEntry node) { + check(); + nodes.remove(node.getIpAddress()); + nodeStates.remove(node.getIpAddress()); + nodeStateTexts.remove(node.getIpAddress()); + } + + @Override + public void removeAllNodes(Collection nodes) { + check(); + nodes.stream() + .filter(Objects::nonNull) + .forEach(this::removeNode); + } + + @Override + public void clear() { + check(); + nodes.clear(); + } + + @Override + public NODE_STATE getNodeSelfHealingState(@NonNull NodeRegistryEntry node) { + check(); + if (mode!=MODE.EXCLUDED && ! nodes.containsKey(node.getIpAddress())) + return NODE_STATE.NOT_MONITORED; + if (mode==MODE.EXCLUDED && nodes.containsKey(node.getIpAddress())) + return NODE_STATE.NOT_MONITORED; + return nodeStates.get(node.getIpAddress()); + } + + @Override + public String getNodeSelfHealingStateText(@NonNull NodeRegistryEntry node) { + check(); + if (mode!=MODE.EXCLUDED && ! nodes.containsKey(node.getIpAddress())) + return null; + if (mode==MODE.EXCLUDED && nodes.containsKey(node.getIpAddress())) + return null; + return nodeStateTexts.get(node.getIpAddress()); + } + + @Override + public void setNodeSelfHealingState(@NonNull NodeRegistryEntry node, @NonNull NODE_STATE state, String text) { + check(); + if (!isMonitored(node)) return; + if (state==NODE_STATE.NOT_MONITORED) + throw new IllegalArgumentException("Node self-healing state cannot be set to NOT_MONITORED. Remove/Exclude node from self-healing instead"); + nodeStates.put(node.getIpAddress(), state); + nodeStateTexts.put(node.getIpAddress(), text); + } +} diff --git a/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/ServerSelfHealingProperties.java b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/ServerSelfHealingProperties.java new file mode 100644 index 0000000..554b64d --- /dev/null +++ b/ems-core/baguette-client-install/src/main/java/gr/iccs/imu/ems/baguette/client/selfhealing/ServerSelfHealingProperties.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.selfhealing; + +import gr.iccs.imu.ems.common.recovery.SelfHealingProperties; +import gr.iccs.imu.ems.common.selfhealing.SelfHealingManager; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Data +@ToString(callSuper=true) +@EqualsAndHashCode(callSuper = true) +@Configuration +public class ServerSelfHealingProperties extends SelfHealingProperties implements InitializingBean { + private SelfHealingManager.MODE mode = SelfHealingManager.MODE.INCLUDED; + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("ServerSelfHealingProperties: {}", this); + } +} diff --git a/ems-core/baguette-client/LICENSE b/ems-core/baguette-client/LICENSE new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/ems-core/baguette-client/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/ems-core/baguette-client/bin/baguette-client b/ems-core/baguette-client/bin/baguette-client new file mode 100644 index 0000000..b7528c1 --- /dev/null +++ b/ems-core/baguette-client/bin/baguette-client @@ -0,0 +1,46 @@ +#! /bin/sh +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +### BEGIN INIT INFO +# Provides: baguette-client +# Required-Start: $local_fs $network +# Required-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: baguette-client +# Description: Controls the Baguette Client service +### END INIT INFO + +export JAVA_HOME="/usr/bin/java" +SU_USER=root + +#startcmd='/opt/baguette-client/bin/run.sh &>>/opt/baguette-client/logs/output.txt &' +#stopcmd='/opt/baguette-client/bin/kill.sh &>>/opt/baguette-client/logs/output.txt' +startcmd='/opt/baguette-client/bin/run.sh' +stopcmd='/opt/baguette-client/bin/kill.sh' + +case "$1" in +start) + echo "Starting Baguette Client..." + su -c "${startcmd}" $SU_USER +;; +restart) + echo "Re-starting Baguette Client..." + su -c "${stopcmd}" $SU_USER + su -c "${startcmd}" $SU_USER +;; +stop) + echo "Stopping Baguette Client..." + su -c "${stopcmd}" $SU_USER +;; +*) + echo "Usage: $0 {start|stop|restart}" +exit 1 +esac diff --git a/ems-core/baguette-client/bin/client.sh b/ems-core/baguette-client/bin/client.sh new file mode 100644 index 0000000..6e6c62d --- /dev/null +++ b/ems-core/baguette-client/bin/client.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) +JAVA_HOME=$( cd ${BASEDIR}/jre* && pwd ) +EMS_CONFIG_DIR=. + +#JAVA_OPTS=-Djavax.net.ssl.trustStore=./broker-truststore.p12\ -Djavax.net.ssl.trustStorePassword=melodic\ -Djavax.net.ssl.trustStoreType=pkcs12 +# -Djavax.net.debug=all +# -Djavax.net.debug=ssl,handshake,record + +${JAVA_HOME}/bin/java $JAVA_OPTS -jar ${BASEDIR}/jars/broker-client/broker-client-jar-with-dependencies.jar $* diff --git a/ems-core/baguette-client/bin/install.sh b/ems-core/baguette-client/bin/install.sh new file mode 100644 index 0000000..9c841dd --- /dev/null +++ b/ems-core/baguette-client/bin/install.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +INSTALL_LOG=/opt/baguette-install.log +echo "START: `date -Iseconds`" >> $INSTALL_LOG + +# Command line arguments: +SERVER_CERT=$1 +BASE_URL=$2 +APIKEY=$3 + +if [ -z "$SERVER_CERT" ]; then + SERVER_CERT="" +elif [ "$SERVER_CERT" = "-" ]; then + SERVER_CERT="--no-check-certificate" +else + SERVER_CERT="--ca-certificate=${SERVER_CERT}" +fi + +# Create installation directories +BIN_DIRECTORY=/opt/baguette-client/bin +CONF_DIRECTORY=/opt/baguette-client/conf +LOGS_DIRECTORY=/opt/baguette-client/logs + +mkdir -p $BIN_DIRECTORY/ +mkdir -p $CONF_DIRECTORY/ +mkdir -p $LOGS_DIRECTORY/ + +echo "" +echo "** EMS Baguette Client **" +echo "** Copyright ICCS-NTUA (C) 2016-2019, http://imu.iccs.gr **" +echo "" +date -Iseconds + +# Common variables +DOWNLOAD_URL=$BASE_URL/baguette-client.tgz +DOWNLOAD_URL_MD5=$BASE_URL/baguette-client.tgz.md5 +INSTALL_PACKAGE=/opt/baguette-client/baguette-client.tgz +INSTALL_PACKAGE_MD5=/opt/baguette-client/baguette-client.tgz.md5 +INSTALL_DIR=/opt/ +STARTUP_SCRIPT=$BIN_DIRECTORY/baguette-client +SERVICE_NAME=baguette-client +CLIENT_CONF_FILE=$CONF_DIRECTORY/baguette-client.properties +CLIENT_ID_FILE=$CONF_DIRECTORY/id.txt + +# Check if already installed +if [ -f /opt/baguette-client/conf/ok.txt ]; then + echo "Already installed. Exiting..." + date -Iseconds + echo "END: Already installed: `date -Iseconds`" >> $INSTALL_LOG + exit 0 +fi + +# Create installation directory +echo "" +echo "Create installation directory..." +date -Iseconds +mkdir -p $INSTALL_DIR/baguette-client +if [ $? != 0 ]; then + echo "Failed to create installation directory ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: mkdir: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi + +# Download installation package +echo "" +echo "Download installation package..." +date -Iseconds +wget $SERVER_CERT $DOWNLOAD_URL -O $INSTALL_PACKAGE +if [ $? != 0 ]; then + echo "Failed to download installation package ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: download: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi +date -Iseconds +echo "Download installation package...ok" + +# Download installation package MD5 checksum +echo "" +echo "Download installation package MD5 checksum..." +date -Iseconds +wget $SERVER_CERT $DOWNLOAD_URL_MD5 -O $INSTALL_PACKAGE_MD5 +if [ $? != 0 ]; then + echo "Failed to download installation package ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: download MD5: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi +date -Iseconds +echo "Download installation package MD5 checksum...ok" + +# Check MD5 checksum +PACKAGE_MD5=`cat $INSTALL_PACKAGE_MD5` +PACKAGE_CHECKSUM=`md5sum $INSTALL_PACKAGE |cut -d " " -f 1` +echo "" +echo "Checksum MD5: $PACKAGE_MD5" +echo "Checksum calc: $PACKAGE_CHECKSUM" +if [ $PACKAGE_CHECKSUM == $PACKAGE_MD5 ]; then + echo "Checksum: ok" +else + echo "Checksum: wrong" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: wrong MD5: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi + +# Extract installation package +echo "" +echo "Extracting installation package..." +date -Iseconds +#unzip -o $INSTALL_PACKAGE -d $INSTALL_DIR +tar -xvzf $INSTALL_PACKAGE -C $INSTALL_DIR +if [ $? != 0 ]; then + echo "Failed to extract installation package contents ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: extract: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi +date -Iseconds + +# Make scripts executable +echo "" +echo "Make scripts executable..." +date -Iseconds +chmod u=rx,og-rwx $INSTALL_DIR/baguette-client/bin/* +if [ $? != 0 ]; then + echo "Failed to copy service script to /etc/init.d/ directory ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: chmod: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi + +# Register as a service +echo "" +echo "Register as a service..." +date -Iseconds +cp -f $STARTUP_SCRIPT /etc/init.d/ +if [ $? != 0 ]; then + echo "Failed to copy service script to /etc/init.d/ directory ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: cp init.d: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi + +update-rc.d $SERVICE_NAME defaults +if [ $? != 0 ]; then + echo "Failed to register service script to /etc/init.d/ directory ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: update-rc.d: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi + +# Add Id, Credentials and Client configuration files +echo "Add Id, Credentials and Client configuration files" +date -Iseconds +touch $CLIENT_ID_FILE $CLIENT_CONF_FILE +if [ $? != 0 ]; then + echo "Failed to 'touch' configuration files ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: touch: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi + +chmod u=rw,og-rwx $CLIENT_ID_FILE $CLIENT_CONF_FILE +if [ $? != 0 ]; then + echo "Failed to change permissions of configuration files ($?)" + echo "Aborting installation..." + date -Iseconds + echo "ABORT: chmod 2: `date -Iseconds`" >> $INSTALL_LOG + exit 1 +fi + +# Write successful installation file +echo "Write successful installation file" +date -Iseconds +sudo touch $CONF_DIRECTORY/ok.txt + +echo "END: OK: `date -Iseconds`" >> $INSTALL_LOG + +# Launch Baguette Client +echo "Launch Baguette Client" +date -Iseconds +sudo service baguette-client start + +echo "RUN: `date -Iseconds`" >> $INSTALL_LOG + +# Success +echo "" +echo "Success - Baguette client successfully installed on system" +date -Iseconds +echo "" +exit 0 diff --git a/ems-core/baguette-client/bin/kill.sh b/ems-core/baguette-client/bin/kill.sh new file mode 100644 index 0000000..b113ad0 --- /dev/null +++ b/ems-core/baguette-client/bin/kill.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# Get Baguette client home directory +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) + +# Update path +#PATH=$PATH:/path/to/jre/bin/ + +# Kill Baguette client +#PID=`jps | grep BaguetteClient | cut -d " " -f 1` +PID=`ps -ef |grep java |grep BaguetteClient | cut -c 10-20` +if [ "$PID" != "" ] +then + echo "Killing baguette client (pid: $PID)" + kill -9 $PID +else + echo "Baguette client is not running" +fi diff --git a/ems-core/baguette-client/bin/run.bat b/ems-core/baguette-client/bin/run.bat new file mode 100644 index 0000000..a38a299 --- /dev/null +++ b/ems-core/baguette-client/bin/run.bat @@ -0,0 +1,44 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +setlocal +set PWD=%~dp0 +cd %PWD%.. +set BASEDIR=%cd% +IF NOT DEFINED EMS_CONFIG_DIR set EMS_CONFIG_DIR=%BASEDIR%\conf +IF NOT DEFINED PAASAGE_CONFIG_DIR set PAASAGE_CONFIG_DIR=%BASEDIR%\conf +IF NOT DEFINED EMS_CONFIG_LOCATION set EMS_CONFIG_LOCATION=optional:file:%EMS_CONFIG_DIR%\ems-client.yml,optional:file:%EMS_CONFIG_DIR%\ems-client.properties,optional:file:%EMS_CONFIG_DIR%\baguette-client.yml,optional:file:%EMS_CONFIG_DIR%\baguette-client.properties +IF NOT DEFINED JASYPT_PASSWORD set JASYPT_PASSWORD=password +set JAVA_HOME=%BASEDIR%/jre + +:: Update path +set PATH=%JAVA_HOME%\bin;%PATH% + +:: Copy dependencies if missing +if exist pom.xml ( + if not exist %BASEDIR%\target\dependency cmd /C "mvn dependency:copy-dependencies" +) + +:: Run Baguette Client +set JAVA_OPTS= -Djavax.net.ssl.trustStore=%EMS_CONFIG_DIR%\client-broker-truststore.p12 ^ + -Djavax.net.ssl.trustStorePassword=melodic ^ + -Djavax.net.ssl.trustStoreType=pkcs12 ^ + -Djasypt.encryptor.password=%JASYPT_PASSWORD% ^ + --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED +::set JAVA_OPTS=-Djavax.net.debug=all %JAVA_OPTS% +::set JAVA_OPTS=-Dlogging.level.gr.iccs.imu.ems=TRACE %JAVA_OPTS% + +echo EMS_CONFIG_DIR=%EMS_CONFIG_DIR% +echo EMS_CONFIG_LOCATION=%EMS_CONFIG_LOCATION% +echo Starting baguette client... +java %JAVA_OPTS% -classpath "%EMS_CONFIG_DIR%;%BASEDIR%\jars\*;%BASEDIR%\target\classes;%BASEDIR%\target\dependency\*" gr.iccs.imu.ems.baguette.client.BaguetteClient "--spring.config.location=%EMS_CONFIG_LOCATION%" "--logging.config=file:%EMS_CONFIG_DIR%\logback-spring.xml" %* + +cd %PWD% +endlocal \ No newline at end of file diff --git a/ems-core/baguette-client/bin/run.sh b/ems-core/baguette-client/bin/run.sh new file mode 100644 index 0000000..e9cd9fc --- /dev/null +++ b/ems-core/baguette-client/bin/run.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# Change directory to Baguette client home +PREVWORKDIR=`pwd` +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) +cd ${BASEDIR} +EMS_CONFIG_DIR=${BASEDIR}/conf +PAASAGE_CONFIG_DIR=${BASEDIR}/conf +EMS_CONFIG_LOCATION=optional:file:$EMS_CONFIG_DIR/ems-client.yml,optional:file:$EMS_CONFIG_DIR/ems-client.properties,optional:file:$EMS_CONFIG_DIR/baguette-client.yml,optional:file:$EMS_CONFIG_DIR/baguette-client.properties +LOG_FILE=${BASEDIR}/logs/output.txt +TEE_FILE=${BASEDIR}/logs/tee.txt +JASYPT_PASSWORD=password +JAVA_HOME=${BASEDIR}/jre +export EMS_CONFIG_DIR PAASAGE_CONFIG_DIR LOG_FILE JASYPT_PASSWORD JAVA_HOME + +# Update path +PATH=${JAVA_HOME}/bin:$PATH + +# Check if baguette client is already running +#PID=`jps | grep BaguetteClient | cut -d " " -f 1` +PID=`ps -ef |grep java |grep BaguetteClient | cut -c 10-14` +if [ "$PID" != "" ] +then + echo "Baguette client is already running (pid: $PID)" + exit 0 +fi + +# Copy dependencies if missing +if [ -f pom.xml ]; then + if [ ! -d ${BASEDIR}/target/dependency ]; then + mvn dependency:copy-dependencies + fi +fi + +# Run Baguette client +JAVA_OPTS=-Djavax.net.ssl.trustStore=${EMS_CONFIG_DIR}/client-broker-truststore.p12 +JAVA_OPTS="${JAVA_OPTS} -Djavax.net.ssl.trustStorePassword=melodic -Djavax.net.ssl.trustStoreType=pkcs12" +JAVA_OPTS="${JAVA_OPTS} -Djasypt.encryptor.password=$JASYPT_PASSWORD" +#JAVA_OPTS="-Djavax.net.debug=all ${JAVA_OPTS}" +#JAVA_OPTS="-Dlogging.level.gr.iccs.imu.ems=TRACE ${JAVA_OPTS}" +JAVA_OPTS="${JAVA_OPTS} --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED" + +echo "Starting baguette client..." +echo "EMS_CONFIG_DIR=${EMS_CONFIG_DIR}" +echo "EMS_CONFIG_LOCATION=${EMS_CONFIG_LOCATION}" +echo "LOG_FILE=${LOG_FILE}" + +echo "Starting baguette client..." &>> ${LOG_FILE} +echo "EMS_CONFIG_DIR=${EMS_CONFIG_DIR}" &>> ${LOG_FILE} +echo "EMS_CONFIG_LOCATION=${EMS_CONFIG_LOCATION}" &>> ${LOG_FILE} +echo "LOG_FILE=${LOG_FILE}" &>> ${LOG_FILE} + +if [ "$1" == "--i" ]; then + echo "Baguette client running in Interactive mode" + java ${JAVA_OPTS} -classpath "conf:jars/*:target/classes:target/dependency/*" gr.iccs.imu.ems.baguette.client.BaguetteClient "--spring.config.location=${EMS_CONFIG_LOCATION}" "--logging.config=file:${EMS_CONFIG_DIR}/logback-spring.xml" $* $* 2>&1 | tee ${TEE_FILE} +else + java ${JAVA_OPTS} -classpath "conf:jars/*:target/classes:target/dependency/*" gr.iccs.imu.ems.baguette.client.BaguetteClient "--spring.config.location=${EMS_CONFIG_LOCATION}" "--logging.config=file:${EMS_CONFIG_DIR}/logback-spring.xml" $* &>> ${LOG_FILE} & + PID=`jps | grep BaguetteClient | cut -d " " -f 1` + PID=`ps -ef |grep java |grep BaguetteClient | cut -c 10-14` + echo "Baguette client PID: $PID" +fi + +cd $PREVWORKDIR \ No newline at end of file diff --git a/ems-core/baguette-client/conf/baguette-client.properties.sample b/ems-core/baguette-client/conf/baguette-client.properties.sample new file mode 100644 index 0000000..ab88d1c --- /dev/null +++ b/ems-core/baguette-client/conf/baguette-client.properties.sample @@ -0,0 +1,214 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +################################################################################ +### EMS - Baguette Client properties ### +################################################################################ + +#password-encoder-class = password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder +#password-encoder-class = password.gr.iccs.imu.ems.util.IdentityPasswordEncoder +#password-encoder-class = password.gr.iccs.imu.ems.util.PresentPasswordEncoder + +# Baguette Client configuration + +auth-timeout = 60000 +exec-timeout = 120000 +#retry-period = 60000 +exit-command-allowed = false +#kill-delay = 10 + +IP_SETTING=${IP_SETTING} +EMS_CLIENT_ADDRESS=${${IP_SETTING}} + +node-properties= + +# ----------------------------------------------------------------------------- +# Client Id and Baguette Server credentials +# ----------------------------------------------------------------------------- + +client-id = ${BAGUETTE_CLIENT_ID} + +#server-address = ${BAGUETTE_SERVER_HOSTNAME} +server-address = ${BAGUETTE_SERVER_ADDRESS} +server-port = ${BAGUETTE_SERVER_PORT} +server-pubkey = ${BAGUETTE_SERVER_PUBKEY} +server-fingerprint = ${BAGUETTE_SERVER_PUBKEY_FINGERPRINT} + +server-username = ${BAGUETTE_SERVER_USERNAME} +server-password = ${BAGUETTE_SERVER_PASSWORD} + +# ----------------------------------------------------------------------------- +# Client-side Self-healing settings +# ----------------------------------------------------------------------------- + +#self.healing.enabled=true +#self.healing.recovery.file.baguette=conf/baguette.json +#self.healing.recovery.file.netdata=conf/netdata.json +#self.healing.recovery.delay=10000 +#self.healing.recovery.retry.wait=60000 +#self.healing.recovery.max.retries=3 + +# ----------------------------------------------------------------------------- +# Collectors settings +# ----------------------------------------------------------------------------- + +#collector-classes = netdata.collector.gr.iccs.imu.ems.baguette.client.NetdataCollector + +collector.netdata.enable = true +collector.netdata.delay = 10000 +collector.netdata.url = http://127.0.0.1:19999/api/v1/allmetrics?format=json +collector.netdata.urlOfNodesWithoutClient = http://%s:19999/api/v1/allmetrics?format=json +#collector.netdata.create-topic = true +#collector.netdata.allowed-topics = netdata__system__cpu__user:an_alias +collector.netdata.allowed-topics = ${COLLECTOR_ALLOWED_TOPICS} +collector.netdata.error-limit = 3 +collector.netdata.pause-period = 60 + +collector.prometheus.enable = true +collector.prometheus.delay = 10000 +collector.prometheus.url = http://127.0.0.1:9090/metrics +collector.prometheus.urlOfNodesWithoutClient = http://%s:9090/metrics +#collector.prometheus.create-topic = true +#collector.prometheus.allowed-topics = system__cpu__user:an_alias +collector.prometheus.allowed-topics = ${COLLECTOR_ALLOWED_TOPICS} +collector.prometheus.error-limit = 3 +collector.prometheus.pause-period = 60 +# +#collector.prometheus.allowedTags = +#collector.prometheus.allowTagsInDestinationName = true +#collector.prometheus.destinationNameFormatter = ${metricName}_${method} +#collector.prometheus.addTagsAsEventProperties = true +#collector.prometheus.addTagsInEventPayload = true +#collector.prometheus.throwExceptionWhenExcessiveCharsOccur = true + +# ----------------------------------------------------------------------------- +# Cluster settings +# ----------------------------------------------------------------------------- + +#cluster.cluster-id=cluster +#cluster.local-node.id=local-node +#cluster.local-node.address=localhost:1234 +#cluster.local-node.properties.name=value +#cluster.member-addresses=[localhost:3456, localhost:5678] + +#cluster.useSwim=false +#cluster.failureTimeout=5000 +cluster.testInterval=5000 + +cluster.log-enabled=true +cluster.out-enabled=true + +cluster.join-on-init=true +cluster.election-on-join=false +#cluster.usePBInMg=true +#cluster.usePBInPg=true +#cluster.mgName=system +#cluster.pgName=data + +cluster.tls.enabled=true +#cluster.tls.keystore=${EMS_CONFIG_DIR}/cluster.jks +#cluster.tls.keystore-password=atomix +#cluster.tls.truststore=${EMS_CONFIG_DIR}/cluster.jks +#cluster.tls.truststore-password=atomix +cluster.tls.keystore-dir=conf + +cluster.score.formula=20*cpu/32+80*ram/(256*1024) +cluster.score.default-score=0 +cluster.score.default-args.cpu=1 +cluster.score.default-args.ram=128 +#cluster.score.throw-exception=false + + +################################################################################ +### EMS - Broker-CEP properties ### +################################################################################ + +# Broker ports and protocol +brokercep.broker-name = broker +brokercep.broker-port = 61617 +#brokercep.management-connector-port = 1088 +brokercep.broker-protocol = ssl +# Don't use in EMS server +#brokercep.bypass-local-broker = true + +# Common Broker settings +BROKER_URL_PROPERTIES = transport.daemon=true&transport.trace=false&transport.useKeepAlive=true&transport.useInactivityMonitor=false&transport.needClientAuth=${CLIENT_AUTH_REQUIRED}&transport.verifyHostName=true&transport.connectionTimeout=0&transport.keepAlive=true +CLIENT_AUTH_REQUIRED = false +brokercep.broker-url[0] = ${brokercep.broker-protocol}://0.0.0.0:${brokercep.broker-port}?${BROKER_URL_PROPERTIES} +brokercep.broker-url[1] = tcp://127.0.0.1:61616?${BROKER_URL_PROPERTIES} +brokercep.broker-url[2] = + +CLIENT_URL_PROPERTIES=daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true +brokercep.broker-url-for-consumer = tcp://127.0.0.1:61616?${CLIENT_URL_PROPERTIES} +brokercep.broker-url-for-clients = ${brokercep.broker-protocol}://${EMS_CLIENT_ADDRESS}:${brokercep.broker-port}?${CLIENT_URL_PROPERTIES} +# Must be a public IP address + +# Key store +brokercep.ssl.keystore-file = ${EMS_CONFIG_DIR}/client-broker-keystore.p12 +brokercep.ssl.keystore-type = PKCS12 +#brokercep.ssl.keystore-password = melodic +brokercep.ssl.keystore-password = ENC(ISMbn01HVPbtRPkqm2Lslg==) +# Trust store +brokercep.ssl.truststore-file = ${EMS_CONFIG_DIR}/client-broker-truststore.p12 +brokercep.ssl.truststore-type = PKCS12 +#brokercep.ssl.truststore-password = melodic +brokercep.ssl.truststore-password = ENC(ISMbn01HVPbtRPkqm2Lslg==) +# Certificate +brokercep.ssl.certificate-file = ${EMS_CONFIG_DIR}/client-broker.crt +# Key-and-Cert data +brokercep.ssl.key-entry-generate = IF-IP-CHANGED +brokercep.ssl.key-entry-name = ${EMS_CLIENT_ADDRESS} +brokercep.ssl.key-entry-dname = CN=${EMS_CLIENT_ADDRESS},OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR +brokercep.ssl.key-entry-ext-san = dns:localhost,ip:127.0.0.1,ip:${DEFAULT_IP},ip:${PUBLIC_IP} + +# Authentication and Authorization settings +brokercep.authentication-enabled = true +#brokercep.additional-broker-credentials = aaa/111, bbb/222, morphemic/morphemic +brokercep.additional-broker-credentials = ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ) +brokercep.authorization-enabled = false + +# Broker instance settings +brokercep.broker-persistence-enabled = false +brokercep.broker-using-jmx = true +brokercep.broker-advisory-support-enabled = true +brokercep.broker-using-shutdown-hook = false + +#brokercep.broker-enable-statistics = true +#brokercep.broker-populate-jmsx-user-id = true + +# Message interceptors +brokercep.message-interceptors[0].destination = > +brokercep.message-interceptors[0].className = interceptor.broker.gr.iccs.imu.ems.brokercep.SequentialCompositeInterceptor +brokercep.message-interceptors[0].params[0] = #SourceAddressMessageUpdateInterceptor +brokercep.message-interceptors[0].params[1] = #MessageForwarderInterceptor +brokercep.message-interceptors[0].params[2] = #NodePropertiesMessageUpdateInterceptor + +brokercep.message-interceptors-specs.SourceAddressMessageUpdateInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.SourceAddressMessageUpdateInterceptor +brokercep.message-interceptors-specs.MessageForwarderInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.MessageForwarderInterceptor +brokercep.message-interceptors-specs.NodePropertiesMessageUpdateInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.NodePropertiesMessageUpdateInterceptor + +# Message forward destinations (MessageForwarderInterceptor must be included in 'message-interceptors' property) +#brokercep.message-forward-destinations[0].connection-string = tcp://localhost:51515 +#brokercep.message-forward-destinations[0].username = AAA +#brokercep.message-forward-destinations[0].password = 111 +#brokercep.message-forward-destinations[1].connection-string = tcp://localhost:41414 +#brokercep.message-forward-destinations[1].username = AAA +#brokercep.message-forward-destinations[1].password = 111 + +# Advisory watcher +brokercep.enable-advisory-watcher = true + +# Memory usage limit +brokercep.usage.memory.jvm-heap-percentage = 20 +#brokercep.usage.memory.size = 134217728 + +#brokercep.maxEventForwardRetries: -1 +#brokercep.maxEventForwardDuration: -1 + +################################################################################ \ No newline at end of file diff --git a/ems-core/baguette-client/conf/baguette-client.yml b/ems-core/baguette-client/conf/baguette-client.yml new file mode 100644 index 0000000..0920f57 --- /dev/null +++ b/ems-core/baguette-client/conf/baguette-client.yml @@ -0,0 +1,246 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +################################################################################ +### EMS - Baguette Client properties ### +################################################################################ + +#password-encoder-class: password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder +#password-encoder-class: password.gr.iccs.imu.ems.util.IdentityPasswordEncoder +#password-encoder-class: password.gr.iccs.imu.ems.util.PresentPasswordEncoder + +# Baguette Client configuration + +auth-timeout: 60000 +exec-timeout: 120000 +#retry-period: 60000 +exit-command-allowed: false +#kill-delay: 10 + +IP_SETTING: ${IP_SETTING} +EMS_CLIENT_ADDRESS: ${${IP_SETTING}} + +node-properties: + node-id: ${NODE_CLIENT_ID} + public-ip: ${NODE_ADDRESS} + private-ip: ${NODE_ADDRESS} + instance: ${NODE_ADDRESS} + host: ${NODE_ADDRESS} + zone: ${zone-id} + region: ${zone-id} + cloud: ${provider} + +# ----------------------------------------------------------------------------- +# Client Id and Baguette Server credentials +# ----------------------------------------------------------------------------- + +client-id: ${BAGUETTE_CLIENT_ID} + +#server-address: ${BAGUETTE_SERVER_HOSTNAME} +server-address: ${BAGUETTE_SERVER_ADDRESS} +server-port: ${BAGUETTE_SERVER_PORT} +server-pubkey: ${BAGUETTE_SERVER_PUBKEY} +server-fingerprint: ${BAGUETTE_SERVER_PUBKEY_FINGERPRINT} + +server-username: ${BAGUETTE_SERVER_USERNAME} +server-password: ${BAGUETTE_SERVER_PASSWORD} + +# ----------------------------------------------------------------------------- +# Client-side Self-healing settings +# ----------------------------------------------------------------------------- + +#self.healing: +# enabled: true +# recovery: +# file: +# baguette: conf/baguette.json +# netdata: conf/netdata.json +# delay: 10000 +# retry-delay: 60000 +# max-retries: 3 + +# ----------------------------------------------------------------------------- +# Collectors settings +# ----------------------------------------------------------------------------- + +#collector-classes: netdata.collector.gr.iccs.imu.ems.baguette.client.NetdataCollector + +collector: + netdata: + enable: true + delay: 10000 + url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + urlOfNodesWithoutClient: http://%s:19999/api/v1/allmetrics?format=json + #create-topic: true + #allowed-topics: netdata__system__cpu__user:an_alias + allowed-topics: ${COLLECTOR_ALLOWED_TOPICS} + error-limit: 3 + pause-period: 60 + prometheus: + enable: true + delay: 10000 + url: http://127.0.0.1:9090/metrics + urlOfNodesWithoutClient: http://%s:9090/metrics + #create-topic: true + #allowed-topics: system__cpu__user:an_alias + allowed-topics: ${COLLECTOR_ALLOWED_TOPICS} + error-limit: 3 + pause-period: 60 + # + #allowedTags: [] + #allowTagsInDestinationName: true + #destinationNameFormatter: '${metricName}_${method}' + #addTagsAsEventProperties: true + #addTagsInEventPayload: true + #throwExceptionWhenExcessiveCharsOccur: true + +# ----------------------------------------------------------------------------- +# Cluster settings +# ----------------------------------------------------------------------------- + +cluster: + #cluster-id: cluster + #local-node.id: local-node + #local-node.address: localhost:1234 + #local-node.properties: + # name: value + #member-addresses: [localhost:3456, localhost:5678] + + #useSwim: false + #failureTimeout: 5000 + testInterval: 5000 + + log-enabled: true + out-enabled: true + + join-on-init: true + election-on-join: false + #usePBInMg: true + #usePBInPg: true + #mgName: system + #pgName: data + + tls: + enabled: true + #keystore: ${EMS_CONFIG_DIR}/cluster.jks + #keystore-password: atomix + #truststore: ${EMS_CONFIG_DIR}/cluster.jks + #truststore-password: atomix + keystore-dir: conf + + score: + formula: 20*cpu/32+80*ram/(256*1024) + default-score: 0 + default-args: + cpu: 1 + ram: 128 + #throw-exception: false + + +################################################################################ +### EMS - Broker-CEP properties ### +################################################################################ + +BROKER_URL_PROPERTIES: transport.daemon=true&transport.trace=false&transport.useKeepAlive=true&transport.useInactivityMonitor=false&transport.needClientAuth=${CLIENT_AUTH_REQUIRED}&transport.verifyHostName=true&transport.connectionTimeout=0&transport.keepAlive=true +CLIENT_AUTH_REQUIRED: false +CLIENT_URL_PROPERTIES: daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true + +brokercep: + # Broker ports and protocol + broker-name: broker + broker-port: 61617 + broker-protocol: ssl + #management-connector-port: 1088 + #bypass-local-broker: true # Don't use in EMS server + + # Broker connectors + broker-url: + - ${brokercep.broker-protocol}://0.0.0.0:${brokercep.broker-port}?${BROKER_URL_PROPERTIES} + - tcp://127.0.0.1:61616?${BROKER_URL_PROPERTIES} + + # Broker URLs for (EMS) consumer and clients + broker-url-for-consumer: tcp://127.0.0.1:61616?${CLIENT_URL_PROPERTIES} + broker-url-for-clients: ${brokercep.broker-protocol}://${EMS_CLIENT_ADDRESS}:${brokercep.broker-port}?${CLIENT_URL_PROPERTIES} + # Must be a public IP address + + ssl: + # Key store settings + keystore-file: ${EMS_CONFIG_DIR}/client-broker-keystore.p12 + keystore-type: PKCS12 + keystore-password: 'ENC(ISMbn01HVPbtRPkqm2Lslg==)' # melodic + + # Trust store settings + truststore-file: ${EMS_CONFIG_DIR}/client-broker-truststore.p12 + truststore-type: PKCS12 + truststore-password: 'ENC(ISMbn01HVPbtRPkqm2Lslg==)' # melodic + + # Certificate settings + certificate-file: ${EMS_CONFIG_DIR}/client-broker.crt + + # key generation settings + key-entry-generate: IF-IP-CHANGED + key-entry-name: ${EMS_CLIENT_ADDRESS} + key-entry-dname: 'CN=${EMS_CLIENT_ADDRESS},OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR' + key-entry-ext-san: 'dns:localhost,ip:127.0.0.1,ip:${DEFAULT_IP},ip:${PUBLIC_IP}' + + # Authentication and Authorization settings + authentication-enabled: true + #additional-broker-credentials: aaa/111, bbb/222, morphemic/morphemic + additional-broker-credentials: 'ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ)' + authorization-enabled: false + + # Broker instance settings + broker-persistence-enabled: false + broker-using-jmx: true + broker-advisory-support-enabled: true + broker-using-shutdown-hook: false + + #broker-enable-statistics: true + #broker-populate-jmsx-user-id: true + + # Message interceptors + message-interceptors: + - destination: '>' + className: 'interceptor.broker.gr.iccs.imu.ems.brokercep.SequentialCompositeInterceptor' + params: + - '#SourceAddressMessageUpdateInterceptor' + - '#MessageForwarderInterceptor' + - '#NodePropertiesMessageUpdateInterceptor' + + message-interceptors-specs: + SourceAddressMessageUpdateInterceptor: + className: interceptor.broker.gr.iccs.imu.ems.brokercep.SourceAddressMessageUpdateInterceptor + MessageForwarderInterceptor: + className: interceptor.broker.gr.iccs.imu.ems.brokercep.MessageForwarderInterceptor + NodePropertiesMessageUpdateInterceptor: + className: interceptor.broker.gr.iccs.imu.ems.brokercep.NodePropertiesMessageUpdateInterceptor + + # Message forward destinations (MessageForwarderInterceptor must be included in 'message-interceptors' property) + #message-forward-destinations: + # - connection-string: tcp://localhost:51515 + # username: AAA + # password: 111 + # - connection-string: tcp://localhost:41414 + # username: AAA + # password: 111 + + # Advisory watcher + enable-advisory-watcher: true + + # Memory usage limit + usage: + memory: + jvm-heap-percentage: 20 + #size: 134217728 + + # Event forward settings + #maxEventForwardRetries: -1 + #maxEventForwardDuration: -1 + +################################################################################ \ No newline at end of file diff --git a/ems-core/baguette-client/conf/baguette.json b/ems-core/baguette-client/conf/baguette.json new file mode 100644 index 0000000..0e95dd2 --- /dev/null +++ b/ems-core/baguette-client/conf/baguette.json @@ -0,0 +1,16 @@ +[{ + "name": "Initial wait...", + "command": "pwd", + "waitBefore": 0, + "waitAfter": 5000 +}, { + "name": "Sending baguette client kill command...", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/kill.sh", + "waitBefore": 0, + "waitAfter": 2000 +}, { + "name": "Sending baguette client start command...", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/run.sh", + "waitBefore": 0, + "waitAfter": 10000 +}] diff --git a/ems-core/baguette-client/conf/logback-spring.xml b/ems-core/baguette-client/conf/logback-spring.xml new file mode 100644 index 0000000..4ae51e9 --- /dev/null +++ b/ems-core/baguette-client/conf/logback-spring.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + BC> %msg%n + + + + + + + + + + + + + + + + + + + diff --git a/ems-core/baguette-client/conf/netdata.json b/ems-core/baguette-client/conf/netdata.json new file mode 100644 index 0000000..ed40f82 --- /dev/null +++ b/ems-core/baguette-client/conf/netdata.json @@ -0,0 +1,16 @@ +[{ + "name": "Initial wait...", + "command": "pwd", + "waitBefore": 0, + "waitAfter": 5000 +}, { + "name": "Sending Netdata agent kill command...", + "command": "sudo sh -c 'ps -U netdata -o \"pid\" --no-headers | xargs kill -9' ", + "waitBefore": 0, + "waitAfter": 2000 +}, { + "name": "Sending Netdata agent start command...", + "command": "sudo netdata", + "waitBefore": 0, + "waitAfter": 10000 +}] diff --git a/ems-core/baguette-client/logs/output.txt b/ems-core/baguette-client/logs/output.txt new file mode 100644 index 0000000..e69de29 diff --git a/ems-core/baguette-client/pom.xml b/ems-core/baguette-client/pom.xml new file mode 100644 index 0000000..0127385 --- /dev/null +++ b/ems-core/baguette-client/pom.xml @@ -0,0 +1,175 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + baguette-client + EMS - Baguette Client + + + 3.1.12 + + + + + gr.iccs.imu.ems + broker-cep + ${project.version} + + + gr.iccs.imu.ems + broker-client + ${project.version} + + + gr.iccs.imu.ems + common + ${project.version} + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework + spring-web + + + com.github.ulisesbocchio + jasypt-spring-boot-starter + ${jasypt.starter.version} + + + + + org.apache.sshd + apache-sshd + ${apache-sshd.version} + pom + + + org.slf4j + slf4j-jdk14 + + + org.bouncycastle + * + + + org.springframework + * + + + + + org.apache.sshd + sshd-scp + ${apache-sshd.version} + + + + + org.projectlombok + lombok + provided + + + + + io.atomix + atomix + ${atomix.version} + + + com.google.guava + guava + + + + + io.atomix + atomix-raft + ${atomix.version} + + + io.atomix + atomix-primary-backup + ${atomix.version} + + + io.atomix + atomix-gossip + ${atomix.version} + + + io.atomix + atomix-storage + ${atomix.version} + + + + + com.google.guava + guava + ${guava.version} + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + + exec + + + + + gr.iccs.imu.ems.baguette.client.BaguetteClient + maven + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + src/main/assembly/baguette-client-installation-package.xml + + baguette-client + + + + package + + single + + + + + + + + + diff --git a/ems-core/baguette-client/src/main/assembly/baguette-client-installation-package.xml b/ems-core/baguette-client/src/main/assembly/baguette-client-installation-package.xml new file mode 100644 index 0000000..0d63efe --- /dev/null +++ b/ems-core/baguette-client/src/main/assembly/baguette-client-installation-package.xml @@ -0,0 +1,102 @@ + + + + installation-package + + tgz + + + + + ${project.basedir} + + README* + LICENSE* + INSTALLATION* + + unix + + + bin + bin + + * + + unix + 0755 + + + + logs + logs + + * + + unix + + + jars + ${project.build.directory} + + *.jar + + + + jars/broker-client + ${project.parent.basedir}/broker-client/target + + broker-client-jar-with-dependencies.jar + + + + + bin + ${project.parent.basedir}/bin + + sysmon.* + + unix + 0755 + + + + + + jars + + *:pom + + true + false + runtime + + + \ No newline at end of file diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClient.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClient.java new file mode 100644 index 0000000..3856ab5 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClient.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client; + +import gr.iccs.imu.ems.baguette.client.cluster.ClusterManagerProperties; +import gr.iccs.imu.ems.baguette.client.collector.netdata.NetdataCollector; +//import prometheus.collector.gr.iccs.imu.ems.baguette.client.PrometheusCollector; +import gr.iccs.imu.ems.baguette.client.plugin.recovery.SelfHealingPlugin; +import gr.iccs.imu.ems.util.EventBus; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Baguette client + */ +@Slf4j +@EnableScheduling +@SpringBootApplication(scanBasePackages = { + "gr.iccs.imu.ems.baguette.client", "gr.iccs.imu.ems.brokercep", "gr.iccs.imu.ems.common", + "gr.iccs.imu.ems.brokerclient", "gr.iccs.imu.ems.util"}) +@RequiredArgsConstructor +public class BaguetteClient implements ApplicationRunner { + @Getter + private final BaguetteClientProperties baguetteClientProperties; + private final ClusterManagerProperties clusterManagerProperties; + private final ConfigurableApplicationContext applicationContext; + + private final List> DEFAULT_COLLECTORS_LIST = List.of( + NetdataCollector.class//, PrometheusCollector.class + ); + + @Getter + private final List collectorsList = new ArrayList<>(); + + private static int killDelay; + + @Getter + private Sshc client; + + public static void main(String[] args) { + SpringApplication.run(BaguetteClient.class, args); + + forceExit(); + } + + @Bean + @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) + public EventBus eventBus() { + return EventBus.builder().build(); + } + + @Override + public void run(ApplicationArguments args) throws IOException { + log.debug("BaguetteClient: Starting"); + + // Process command line arguments + processCommandLineArgs(args); + killDelay = baguetteClientProperties.getKillDelay(); + log.debug("BaguetteClient: configuration: {}", baguetteClientProperties); + log.debug("Cluster: configuration: {}", clusterManagerProperties); + + boolean interactiveMode = args.containsOption("i"); + + // Start measurement collectors (but not in interactive mode) + if (!interactiveMode) { + startCollectors(); + applicationContext.getBean(SelfHealingPlugin.class).start(); + } + + if (interactiveMode) { + // Run CLI + log.debug("BaguetteClient: Enters interactive mode"); + runCli(); + } else { + // Run SSH client + log.debug("BaguetteClient: Enters SSH mode"); + runSshClient(); + } + log.debug("BaguetteClient: Exiting"); + + // Stop measurement collectors + if (!interactiveMode) { + applicationContext.getBean(SelfHealingPlugin.class).stop(); + stopCollectors(); + } + + // Stop Baguette Client services + applicationContext.close(); + + log.info("BaguetteClient: Bye"); + } + + private void processCommandLineArgs(ApplicationArguments args) { + // Get cluster node addresses and properties + List addresses = args.getNonOptionArgs(); + if (addresses!=null && addresses.size()>0) { + clusterManagerProperties.getLocalNode().setAddress(addresses.get(0)); + if (addresses.size()>1) { + clusterManagerProperties.setMemberAddresses(addresses.subList(1, addresses.size())); + } + } + + // Enable/Disable TLS + if (args.containsOption("tls")) + clusterManagerProperties.getTls().setEnabled(true); + if (args.containsOption("notls")) + clusterManagerProperties.getTls().setEnabled(false); + } + + protected void startCollectors() { + if (!collectorsList.isEmpty()) + throw new IllegalArgumentException("Collectors have already been started"); + + log.debug("BaguetteClient: Starting collectors..."); + if (baguetteClientProperties.getCollectorClasses()==null) + baguetteClientProperties.setCollectorClasses(DEFAULT_COLLECTORS_LIST); + for (Class collectorClass : baguetteClientProperties.getCollectorClasses()) { + try { + log.debug("BaguetteClient: Starting collector: {}...", collectorClass.getName()); + Collector collector = applicationContext.getBean(collectorClass); + collector.start(); + collectorsList.add(collector); + log.debug("BaguetteClient: Starting collector: {}...ok", collectorClass.getName()); + } catch (NoSuchBeanDefinitionException e) { + log.error("BaguetteClient: Exception while starting collector: {}: ", collectorClass.getName(), e); + } + } + log.debug("BaguetteClient: Starting collectors...ok"); + } + + protected void stopCollectors() { + log.debug("BaguetteClient: Stopping collectors..."); + for (Collector collector : collectorsList) { + try { + log.debug("BaguetteClient: Stopping collector: {}...", collector.getClass().getName()); + collector.stop(); + log.debug("BaguetteClient: Stopping collector: {}...ok", collector.getClass().getName()); + } catch (NoSuchBeanDefinitionException e) { + log.error("BaguetteClient: Exception while stopping collector: {}: ", collector.getClass().getName(), e); + } + } + collectorsList.clear(); + } + + protected void runSshClient() { + long retryDelay = baguetteClientProperties.getConnectionRetryDelay(); + boolean retry = baguetteClientProperties.isConnectionRetryEnabled() && retryDelay>=0; + int retryLimit = baguetteClientProperties.getConnectionRetryLimit(); + int retryCount = 0; + while (true) { + try { + // Connect to baguette server + startSshClient(retry); + + // Exchange messages with Baguette server + log.trace("BaguetteClient: Calling SSHC run()"); + client.run(); + retryCount = 0; + + // Disconnect from baguette server + stopSshClient(); + } catch (Exception ex) { + log.error("BaguetteClient: EXCEPTION: ", ex); + } + + // Check if retry is enabled + if (!retry) break; + + // Check if retry limit has been reached + retryCount++; + if (retryLimit>=0 && retryCount > retryLimit) { + log.error("BaguetteClient: Giving up connection retries after {} failed attempts", retryCount-1); + break; + } + + // Wait for a while before retrying to reconnect + try { + Thread.sleep(retryDelay); + } catch (InterruptedException e) { + log.warn("BaguetteClient: Cancelling connection retry"); + break; + } + log.info("BaguetteClient: Retrying to connect (attempt #{})...", retryCount); + } + } + + protected void runCli() throws IOException { + BaguetteClientCLI cli = applicationContext.getBean(BaguetteClientCLI.class); + cli.setConfiguration(baguetteClientProperties); + cli.run(); + } + + public synchronized void startSshClient(boolean retry) throws IOException { + log.trace("BaguetteClient: spring-boot application-context: {}", applicationContext); + client = applicationContext.getBean(Sshc.class); + client.setConfiguration(baguetteClientProperties); + + log.trace("BaguetteClient: Sshc instance from application-context: {}", client); + log.trace("BaguetteClient: Calling SSHC start()"); + client.start(retry); + client.greeting(); + } + + public synchronized void stopSshClient() throws IOException { + log.trace("BaguetteClient: Calling SSHC stop()"); + Sshc tmp = client; + client = null; + tmp.stop(); + } + + /*protected static Properties loadConfig(String configFile) throws IOException { + Properties config = new Properties(); + try { + try (InputStream in = new FileInputStream(new File(configFile))) { + config.load(in); + } + } catch (FileNotFoundException ex) { + try (InputStream in = BaguetteClient.class.getResourceAsStream(configFile)) { + if (in == null) throw ex; + config.load(in); + } + } + return config; + }*/ + + protected static void forceExit() { + // Print remaining threads + Thread.getAllStackTraces().keySet() + .forEach(s -> log.debug("---> {}.{}: {} alive={}, daemon={}, interrupted={}", + s.getThreadGroup().getName(), s.getName(), s.getState(), + s.isAlive(), s.isDaemon(), s.isInterrupted())); + + // Start killer thread + if (killDelay>0) { + new Thread(() -> { + try { Thread.sleep(1000); } catch (InterruptedException ignored) { } + log.warn("Waiting JVM to exit for {} more seconds", killDelay); + try { Thread.sleep(killDelay * 1000); } catch (InterruptedException ignored) { } + log.warn("Forcing JVM to exit"); + System.exit(0); + }) {{ + setDaemon(true); + start(); + }}; + } else { + log.debug("Killer thread disabled"); + } + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClientCLI.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClientCLI.java new file mode 100644 index 0000000..a2e7bf7 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClientCLI.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client; + +import gr.iccs.imu.ems.baguette.client.cluster.ClusterManager; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.*; + +/** + * Baguette Client Command-Line Interface + */ +@Slf4j +@Service +public class BaguetteClientCLI { + private BaguetteClientProperties config; + private String clientId; + private String prompt = "CLI> "; + + @Autowired + private CommandExecutor commandExecutor; + @Autowired + BrokerCepService brokerCepService; + + public void setConfiguration(BaguetteClientProperties config) { + this.config = config; + this.clientId = config.getClientId(); + if (StringUtils.isNotBlank(clientId)) + prompt = "CLI-"+ ClusterManager.getLocalHostName()+" > "; + config.setExitCommandAllowed(true); + log.trace("Sshc: cmd-exec: {}", commandExecutor); + this.commandExecutor.setConfiguration(config); + } + + public void run() throws IOException { + run(System.in, System.out, System.err); + } + + public void run(InputStream in, PrintStream out, PrintStream err) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + out.print(prompt); + out.flush(); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + try { + boolean exit = commandExecutor.execCmd(line.split("[ \t]+"), in, out, err); + if (exit) break; + } catch (Exception ex) { + ex.printStackTrace(out); + out.flush(); + } + out.print(prompt); + out.flush(); + } + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClientProperties.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClientProperties.java new file mode 100644 index 0000000..b2f6037 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/BaguetteClientProperties.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client; + +import gr.iccs.imu.ems.common.client.SshClientProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import java.util.List; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +@Configuration +@ConfigurationProperties +@PropertySource(value = { + "file:${EMS_CONFIG_DIR}/ems-client.yml", + "file:${EMS_CONFIG_DIR}/ems-client.properties", + "file:${EMS_CONFIG_DIR}/baguette-client.yml", + "file:${EMS_CONFIG_DIR}/baguette-client.properties" +}, ignoreResourceNotFound = true) +public class BaguetteClientProperties extends SshClientProperties { + private String baseDir; + + private boolean connectionRetryEnabled = true; + private long connectionRetryDelay = 10 * 1000L; + private int connectionRetryLimit = -1; + + private boolean exitCommandAllowed = false; + private int killDelay = 5; + + private List> collectorClasses; + + private String debugFakeIpAddress; + + private long sendStatisticsDelay = 10000L; +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/Collector.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/Collector.java new file mode 100644 index 0000000..a1a5cbd --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/Collector.java @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client; + +import gr.iccs.imu.ems.util.Plugin; + +public interface Collector extends Plugin { + void activeGroupingChanged(String oldGrouping, String newGrouping); +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/CommandExecutor.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/CommandExecutor.java new file mode 100644 index 0000000..e52e236 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/CommandExecutor.java @@ -0,0 +1,1416 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gr.iccs.imu.ems.baguette.client.cluster.*; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.BrokerCepStatementSubscriber; +import gr.iccs.imu.ems.brokercep.cep.CepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.brokerclient.event.EventGenerator; +import gr.iccs.imu.ems.brokerclient.properties.BrokerClientProperties; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.common.misc.EventConstant; +import gr.iccs.imu.ems.common.misc.SystemResourceMonitor; +import gr.iccs.imu.ems.util.*; +import io.atomix.cluster.ClusterMembershipEvent; +import io.atomix.cluster.Member; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static gr.iccs.imu.ems.util.GroupingConfiguration.BrokerConnectionConfig; + +/** + * Command Executor + */ +@Slf4j +@Service +public class CommandExecutor { + + private static String getConfigDir() { + String confDir = System.getenv("EMS_CONFIG_DIR"); + if (StringUtils.isBlank(confDir)) confDir = System.getProperty("EMS_CONFIG_DIR"); + if (StringUtils.isBlank(confDir)) confDir = "conf"; + return confDir; + } + + private final static String DEFAULT_CONF_DIR = getConfigDir(); + private final static String DEFAULT_ID_FILE = DEFAULT_CONF_DIR + "/cached-id.properties"; + private static final int DEFAULT_ID_LENGTH = 32; + private final static String DEFAULT_KEYSTORE_DIR = DEFAULT_CONF_DIR; + + public final static String EVENT_CLUSTER_NODE_ADDED = "CLUSTER_NODE_ADDED"; + public final static String EVENT_CLUSTER_NODE_REMOVED = "CLUSTER_NODE_REMOVED"; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private BaguetteClient baguetteClient; + @Autowired + private BrokerCepService brokerCepService; + @Autowired + private BrokerClientProperties brokerClientProperties; + @Autowired + private PasswordUtil passwordUtil; + @Autowired + @Getter + private EventBus eventBus; + + private BaguetteClientProperties config; + private String idFile; + + private InputStream in; + private PrintStream out; + private PrintStream err; + private String clientId; + + @Getter + private ClientConfiguration clientConfiguration; + @Getter + private final Map groupings = new LinkedHashMap<>(); + private GroupingConfiguration activeGrouping; + + private final AtomicLong subscriberCount = new AtomicLong(0); + private final Map> groupingsSubscribers = new LinkedHashMap<>(); + + private final Map eventGenerators = new HashMap<>(); + + @Autowired + private ClusterManagerProperties clusterManagerProperties; + @Getter + private ClusterManager clusterManager; + private ClusterTest clusterTest; + private boolean clusterKeystoreInitialized = false; + private String clusterKeystoreFile; + private String clusterKeystoreType; + private String clusterKeystorePassword; + + @Getter private String globalGrouping; + @Getter private String aggregatorGrouping; + @Getter private String nodeGrouping; + + private Thread serverWatcherThread; + private boolean captureInputLine; + @Getter private String lastInputLine; + + @Autowired + private TaskScheduler taskScheduler; + private ScheduledFuture statsSendTask; + @Autowired + private SystemResourceMonitor systemResourceMonitor; + + public CommandExecutor() { + initializeClientId(); + } + + public void setConfiguration(BaguetteClientProperties config) { + log.trace("CommandExecutor: brokerCepService: {}", brokerCepService); + log.trace("CommandExecutor: config: {}", config); + this.config = config; + this.idFile = DEFAULT_ID_FILE; + initializeClientId(); + } + + private void initializeClientId() { + if (config!=null && StringUtils.isNotBlank(config.getClientId())) { + clientId = config.getClientId().trim(); + saveClientId(clientId); + } + if (StringUtils.isBlank(clientId)) + clientId = loadCachedClientId(); + if (StringUtils.isBlank(clientId)) { + this.clientId = RandomStringUtils.randomAlphanumeric(DEFAULT_ID_LENGTH); + saveClientId(clientId); + } + } + + void communicateWithServer(InputStream in, PrintStream out, PrintStream err) throws IOException { + log.trace("communicateWithServer(): BEGIN"); + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line; + while ((line = reader.readLine()) != null) { + log.trace("communicateWithServer(): WHILE START: {}", line); + if (captureInputLine) { + lastInputLine = line; + log.trace("communicateWithServer(): captureInputLine: {}", line); + captureInputLine = false; + continue; + } + line = line.trim(); + if (StringUtils.startsWithIgnoreCase(line, "CLUSTER-KEY")) { + String[] s = line.split(" ", 2); + log.info("Cluster key from Server: {} {}", s[0], s.length>1 ? passwordUtil.encodePassword(s[1]) : ""); + } else + log.info("Server input: {}", line); + + try { + log.trace("communicateWithServer(): Calling execCmd: {}", line); + boolean exit = execCmd(line.split("[ \t]+"), in, out, err); + log.trace("communicateWithServer(): Exit code: {}", exit); + if (exit) break; + } catch (Exception ex) { + log.error("communicateWithServer(): EXCEPTION: ", ex); + // Report exception back to server + err.println(ex); + ex.printStackTrace(err); + err.flush(); + } + log.trace("communicateWithServer(): WHILE END"); + } + log.trace("communicateWithServer(): END"); + } + + public void executeCommand(String command) throws IOException, InterruptedException { + String[] args = command.split(" "); + execCmd(args, baguetteClient.getClient().getIn(), baguetteClient.getClient().getOut(), baguetteClient.getClient().getOut()); + + // Wait for server response/input if needed + while (captureInputLine) { + log.trace("Waiting for server input..."); + try { Thread.sleep(100); } catch (InterruptedException e) {} + } + log.trace("Server input: {}", lastInputLine); + } + + boolean executeCommand(String line, InputStream in, PrintStream out, PrintStream err) throws IOException, InterruptedException { + return execCmd(line.split("[ \t]+"), in, out, err); + } + + boolean execCmd(String[] args, InputStream in, PrintStream out, PrintStream err) throws IOException, InterruptedException { + if (args == null || args.length == 0) return false; + String cmd = args[0].toUpperCase(); + args[0] = ""; + + this.in = in; + this.out = out; + this.err = err; + + if ("EXIT".equals(cmd)) { + boolean canExit = config != null && config.isExitCommandAllowed(); + if (canExit) { + if (clusterManager != null && clusterManager.isRunning()) + clusterManager.leaveCluster(); + return true; // Signal 'Sshc' to quit + } else { + final String mesg = "Exit is not allowed. Ignoring EXIT command"; + log.warn(mesg); + out.println(mesg); + } + } else if ("CONNECT".equals(cmd)) { + if (serverWatcherThread!=null) { + log.warn("Already connected"); + return false; + } + baguetteClient.startSshClient(false); + serverWatcherThread = new Thread(() -> { + BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(baguetteClient.getClient().getIn()))); + String line; + try { + while ((line = reader.readLine()) != null) { + log.info(line); + } + } catch (Exception ex) { + if (baguetteClient.getClient()!=null) + log.warn("Exception in serverWatcherThread: ", ex); + else + log.debug("serverWatcherThread has exited"); + } + serverWatcherThread = null; + }); + serverWatcherThread.start(); + } else if ("DISCONNECT".equals(cmd)) { + if (serverWatcherThread==null) { + log.warn("Not connected"); + return false; + } + baguetteClient.stopSshClient(); + serverWatcherThread = null; + + } else if ("SEND".equals(cmd)) { + StringBuilder sb = new StringBuilder(); + for (int i=1; i1) ? args[1].trim() : DEFAULT_CONF_DIR + "/config-export.json"; + ConfigurationContents contents = ConfigurationContents.builder() + .timestamp(System.currentTimeMillis()) + .clientId(this.clientId) + .activeGrouping(this.activeGrouping.getName()) + .groupings(this.groupings) + .build(); + + ObjectMapper mapper = new ObjectMapper(); + File file = Paths.get(fileName).toFile(); + mapper.writer().writeValue(file, contents); + log.info("Current configuration saved to file: {}", file.getPath()); + + } else if ("READ-CONFIGURATION".equals(cmd)) { + String fileName = (args.length>1) ? args[1].trim() : DEFAULT_CONF_DIR + "/config-export.json"; + File file = Paths.get(fileName).toFile(); + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + + ObjectMapper mapper = new ObjectMapper(); + ConfigurationContents config = mapper.readValue(content, ConfigurationContents.class); + log.debug("Configuration read from file: {}\n{}", file, config); + + // Clear current state + clearGroupings(); + + // Initialize current state + String newId = config.getClientId(); + if (StringUtils.isNotBlank(newId)) + saveClientId(newId); + + config.getGroupings().forEach(groupings::put); + + String activeConf = config.getActiveGrouping(); + if (StringUtils.isNotBlank(activeConf)) + setActiveGrouping(activeConf); + + log.info("Current configuration loaded from file: {}", file.getPath()); + + } else if ("LIST-GROUPING-CONFIGS".equals(cmd)) { + log.info("Configured groupings: {}", groupings.keySet()); + out.println(String.join(", ", groupings.keySet())); + } else if ("CLEAR-GROUPING-CONFIGS".equals(cmd)) { + clearGroupings(); + } else if ("GET-GROUPING-CONFIG".equals(cmd)) { + if (args.length < 2) return false; + GroupingConfiguration grouping = groupings.get(args[1].trim()); + log.info("{}", grouping); + out.printf("%s\n", grouping); + } else if ("SET-CLIENT-CONFIG".equals(cmd)) { + if (args.length < 2) return false; + String configStr = String.join(" ", args).trim(); + log.trace("client-config-base64: {}", configStr); + setClientConfiguration(configStr); + } else if ("SET-GROUPING-CONFIG".equals(cmd)) { + if (args.length < 2) return false; + String configStr = String.join(" ", args).trim(); + log.trace("grouping-config-base64: {}", configStr); + setGroupingConfiguration(configStr); + } else if ("GET-ACTIVE-GROUPING".equals(cmd)) { + String activeGroupingName = activeGrouping != null ? activeGrouping.getName() : "-"; + log.info("Active grouping: {}", activeGroupingName); + out.println(activeGroupingName); + } else if ("SET-ACTIVE-GROUPING".equals(cmd)) { + if (args.length < 2) return false; + String newGrouping = String.join(" ", args).trim(); + log.trace("new-active-grouping: {}", newGrouping); + setActiveGrouping(newGrouping); + } else if ("SET-CONSTANTS".equals(cmd)) { + if (args.length < 2) return false; + String configStr = String.join(" ", args).trim(); + log.trace("constants-base64: {}", configStr); + setConstants(configStr); + } else if ("SEND-LOCAL-EVENT".equals(cmd)) { + if (args.length < 2) return false; + String destination = args[1].trim(); + double value = args.length > 2 ? Double.parseDouble(args[2].trim()) : Math.random() * 1000; + log.trace("Sending local event: destination={}, metricValue={}", destination, value); + sendLocalEvent(destination, value); + } else if ("SEND-EVENT".equals(cmd)) { + if (args.length < 3) return false; + String connection = args[1].trim(); + String destination = args[2].trim(); + double value = args.length > 3 ? Double.parseDouble(args[3].trim()) : Math.random() * 1000; + log.trace("Sending event: connection={}, destination={}, metricValue={}", connection, destination, value); + sendEvent(connection, destination, value); + } else if ("GENERATE-EVENTS-START".equals(cmd)) { + if (args.length < 5) return false; + String destination = args[1].trim(); + long interval = Long.parseLong(args[2].trim()); + double lower = Double.parseDouble(args[3].trim()); + double upper = Double.parseDouble(args[4].trim()); + + if (eventGenerators.get(destination) == null) { + EventGenerator generator = applicationContext.getBean(EventGenerator.class); + generator.setBrokerUrl(brokerCepService.getBrokerCepProperties().getBrokerUrlForClients()); + generator.setBrokerUsername(brokerCepService.getBrokerUsername()); + generator.setBrokerPassword(brokerCepService.getBrokerPassword()); + generator.setDestinationName(destination); + generator.setLevel(1); + generator.setInterval(interval); + generator.setLowerValue(lower); + generator.setUpperValue(upper); + eventGenerators.put(destination, generator); + generator.start(); + } + } else if ("GENERATE-EVENTS-STOP".equals(cmd)) { + if (args.length < 2) return false; + String destination = args[1].trim(); + EventGenerator generator = eventGenerators.remove(destination); + if (generator != null) { + generator.stop(); + } + } else if ("CLUSTER-KEY".equals(cmd)) { + if (args.length<5) { + log.error("Too few arguments"); + return false; + } + + setClusterKeystore(args[1], args[2], args[3], args[4]); + + } else if ("CLUSTER-JOIN".equals(cmd)) { + if (clusterManager!=null && clusterManager.isRunning()) { + log.error("Cluster is running. Leave cluster first"); + return false; + } + + // Check and collect arguments + if (args.length<5) { + log.error("Too few arguments"); + out.println("Too few arguments (CLUSTER-JOIN)"); + return false; + } + List argsList = new ArrayList<>(Arrays.asList(args)); + argsList.remove(0); // Discard command part + String clusterId = argsList.remove(0); + String groupings = argsList.remove(0); + boolean startElection = Boolean.parseBoolean( + StringUtils.substringAfter(argsList.remove(0), "start-election=")); + String localNodeAddress = argsList.remove(0); + List otherNodeAddresses = argsList.isEmpty() ? null : argsList; + log.info("CLUSTER-JOIN ARGS: cluster-id={}, groupings={}, local-node={}, other-nodes={}", + clusterId, groupings, localNodeAddress, otherNodeAddresses); + + // Setup groupings + String[] grpPart = groupings.split(":"); + globalGrouping = grpPart[0]; + aggregatorGrouping = grpPart[1]; + nodeGrouping = grpPart[2]; + log.info("CLUSTER-JOIN ARGS: Groupings: global={}, aggregator={}, node={}", + globalGrouping, aggregatorGrouping, nodeGrouping); + + // Initialize cluster properties + if (clusterManagerProperties==null) + clusterManagerProperties = new ClusterManagerProperties(); + clusterManagerProperties.setClusterId(clusterId); + + if (clusterManagerProperties.getTls().isEnabled()) { + log.debug("Cluster TLS is enabled"); + if (clusterKeystoreInitialized) { + log.debug("Cluster TLS Keystore has been initialized"); + clusterManagerProperties.getTls().setKeystore(clusterKeystoreFile); + clusterManagerProperties.getTls().setKeystorePassword(clusterKeystorePassword); + clusterManagerProperties.getTls().setTruststore(clusterKeystoreFile); + clusterManagerProperties.getTls().setTruststorePassword(clusterKeystorePassword); + } + } + + clusterManagerProperties.getLocalNode().setAddress(localNodeAddress); + clusterManagerProperties.setMemberAddresses(otherNodeAddresses); + log.debug("Cluster properties: {}", clusterManagerProperties); + + // Initialize cluster manager + if (clusterManager==null) { + clusterManager = applicationContext.getBean(ClusterManager.class); + clusterManager.setProperties(clusterManagerProperties); + } + + // Join/start cluster + clusterManager.initialize(clusterManagerProperties, new ClusterNodeCallback(this)); + //clusterManager.setCallback(new TestCallback(clusterManager.getLocalAddress())); + clusterManager.joinCluster( startElection ); + clusterManager.waitToJoin(); + log.info("Joined to cluster"); + + // Set this node's broker connection configuration (Used if it becomes the Aggregator) + String brokerConnConfig = getBrokerConfigurationAsString(); + clusterManager.getLocalMember().properties().setProperty("aggregator-connection-configuration", brokerConnConfig); + + // Update forwards to Aggregator (if any) + List aggregators = clusterManager.getBrokerUtil().getBrokers(); + if (aggregators.size()==1) { + String newConfig = aggregators.get(0).properties().getProperty("aggregator-connection-configuration", ""); + if (StringUtils.isNotBlank(newConfig)) { + setBrokerConfigurationFromString(newConfig); + } else { + log.error("CLUSTERING ERROR: Aggregator broker connection config. is not available: {}", aggregators.get(0)); + } + } else if (aggregators.isEmpty()) { + log.info("No Aggregators found. Waiting for Baguette Server command"); + } else { + log.error("CLUSTERING ERROR: Many Aggregators found! {}", aggregators); + } + + // Update node status based on current grouping + if (activeGrouping==null) + clusterManager.getBrokerUtil().setLocalStatus(BrokerUtil.NODE_STATUS.NOT_SET); + else if (activeGrouping.getName().equals(aggregatorGrouping)) + clusterManager.getBrokerUtil().setLocalStatus(BrokerUtil.NODE_STATUS.AGGREGATOR); + else + clusterManager.getBrokerUtil().setLocalStatus(BrokerUtil.NODE_STATUS.CANDIDATE); + + } else if ("CLUSTER-TEST".equals(cmd)) { + + if (args.length<2 || "START".equalsIgnoreCase(args[1])) { + if (clusterManager==null) { + log.error("Cluster has not been initialized. Run CLUSTER-JOIN first"); + return false; + } + long interval = Math.max(100L, (args.length>=3) + ? Long.parseLong(args[2]) + : clusterManagerProperties.getTestInterval()); + clusterTest = new ClusterTest(clusterManager); + clusterTest.startTest(interval); + } else if ("STOP".equalsIgnoreCase(args[1])) { + if (clusterTest==null) { + log.error("Cluster test is not running"); + return false; + } + clusterTest.stopTest(); + clusterTest = null; + } else { + log.error("Unknown command option: {} {}", cmd, args[1]); + } + + } else if ("CLUSTER-LEAVE".equals(cmd)) { + if (clusterManager==null) { + log.error("Cluster has not been initialized. Run CLUSTER-JOIN first"); + return false; + } + if (! clusterManager.isRunning()) { + log.error("Cluster is not running. Join cluster first"); + return false; + } + + clusterManager.leaveCluster(); + + if (clusterTest!=null) { + clusterTest.stopTest(); + clusterTest = null; + } + log.info("Left cluster"); + + } else if ("CLUSTER-SHELL".equals(cmd)) { + if (clusterManager==null) { + log.error("Cluster has not been initialized. Run CLUSTER-JOIN first"); + return false; + } + ClusterCLI cli = clusterManager.getCli(); + cli.setIn(in); + cli.setOut(out); + cli.setErr(err); + cli.setPromptUpdate(true); + log.info("Cluster CLI starts"); + cli.run(); + log.info("Cluster CLI ended"); + } else if ("CLUSTER-EXEC".equals(cmd)) { + if (args.length < 2) { + log.error("No cluster command specified"); + return false; + } + if (clusterManager==null) { + log.error("Cluster has not been initialized. Run CLUSTER-JOIN first"); + return false; + } + ClusterCLI cli = clusterManager.getCli(); + cli.setIn(in); + cli.setOut(out); + cli.setErr(err); + String[] args1 = Arrays.stream(args, 1, args.length).toArray(String[]::new); + String cmd1 = String.join(" ", args1); + try { + log.info("Cluster executes command: {}", cmd1); + cli.executeCommand(cmd1, args1); + } catch (Exception ex) { + log.error("Cluster: Exception caught while executing command: {}\nException ", cmd1, ex); + } + + } else if ("GET-LOCAL-NODE-CERTIFICATE".equals(cmd)) { + String localAddress = ClusterManager.getLocalHostAddress(); + String localHostname = ClusterManager.getLocalHostName(); + String nlChar = (args.length > 1) ? args[1].trim() : null; + try { + log.info("Retrieving this node certificate from keystore:"); + String cert = brokerCepService.getBrokerCertificate(); + if (cert!=null && StringUtils.isNotBlank(nlChar)) + cert = cert.replace("\r\n", nlChar).replace("\n", nlChar); + log.info("{} {} {}", localAddress, localHostname, cert); + out.println(localAddress+" "+localHostname+" "+cert); + } catch (Exception e) { + log.error("Exception while retrieving local node certificate: ", e); + } + + } else if ("ADD-TRUSTED-NODE".equals(cmd)) { + if (args.length < 4) return false; + String nodeAlias = args[1]; + String nlChar = args[2]; + String nodeCert = String.join(" ", + Arrays.asList(args).subList(3, args.length)).replace(nlChar, "\n"); + try { + log.info("Adding/Updating trusted node certificate in truststore: {}\nCertificate: {}", nodeAlias, nodeCert); + brokerCepService.addOrReplaceCertificateInTruststore(nodeAlias, nodeCert); + log.info("Truststore updated: {}", nodeAlias); + } catch (Exception e) { + log.error("Exception while updating truststore: ", e); + } + + } else if ("DEL-TRUSTED-NODE".equals(cmd)) { + if (args.length < 2) return false; + String nodeAlias = args[1]; + try { + log.info("Deleting trusted node certificate from truststore: {}", nodeAlias); + brokerCepService.deleteCertificateFromTruststore(nodeAlias); + log.info("Truststore updated: {}", nodeAlias); + } catch (Exception e) { + log.error("Exception while updating truststore: ", e); + } + + } else if ("COLLECTOR".equals(cmd)) { + if (args.length < 2) { + log.warn("Too few arguments"); + out.println("Too few arguments"); + return false; + } + String operation = args[1]; + String target = args.length==3 ? args[2] : null; + boolean all = ("*".equalsIgnoreCase(target) || "ALL".equalsIgnoreCase(target)); + if ("LIST".equalsIgnoreCase(operation)) { + String list = baguetteClient.getCollectorsList().stream() + .map(c->" - "+c.getClass().getName()) + .collect(Collectors.joining("\n")); + log.info("BaguetteClient: Listing Collectors:\n{}", list); + out.printf("Listing Collectors:\n%s\n", list); + } else + if ("START".equalsIgnoreCase(operation)) { + if (target==null) { + log.warn("Too few arguments"); + out.println("Too few arguments"); + return false; + } + log.info("BaguetteClient: Starting Collector: {}...", target); + baguetteClient.getCollectorsList().stream() + .filter(c -> all || c.getClass().getName().equals(target)) + .peek(c -> log.debug(" - Starting collector: {}...", c.getClass().getName())) + .forEach(Collector::start); + log.info("BaguetteClient: Starting Collector: {}... done", target); + } else + if ("STOP".equalsIgnoreCase(operation)) { + if (target==null) { + log.warn("Too few arguments"); + out.println("Too few arguments"); + return false; + } + log.info("BaguetteClient: Stopping Collector: {}...", target); + baguetteClient.getCollectorsList().stream() + .filter(c -> all || c.getClass().getName().equals(target)) + .peek(c -> log.debug(" - Stopping collector: {}...", c.getClass().getName())) + .forEach(Collector::stop); + log.info("BaguetteClient: Stopping Collector: {}... done", target); + } else + log.error("BaguetteClient: Unknown Collector operation: {}", operation); + + } else if ("SHOW-CONFIG".equals(cmd)) { + log.info("BaguetteClient: configuration:\n{}", config); + log.info("Cluster: configuration:\n{}", clusterManagerProperties); + } else if ("GET-STATS".equals(cmd)) { + getStatistics(args[1]); + } else if ("SEND-STATS".equals(cmd)) { + if (args.length < 2) { + log.warn("Too few arguments"); + out.println("Too few arguments"); + return false; + } + String operation = args[1]; + + if ("START".equalsIgnoreCase(operation)) + sendStatisticsStart(); + else if ("STOP".equalsIgnoreCase(operation)) + sendStatisticsStop(); + else if ("CLEAR".equalsIgnoreCase(operation)) + clearStatistics(); + else { + log.error("BaguetteClient: Unknown STATS operation: {}", operation); + } + + } else if ("CLEAR-STATS".equals(cmd)) { + clearStatistics(); + } else if ("SEND-CLIENT-PROPERTY".equals(cmd)) { + if (args.length < 2) { + log.warn("Too few arguments"); + out.println("Too few arguments"); + return false; + } + String propName = args[1]; + String propValue = args.length==3 ? args[2] : null; + sendClientProperty(propName, propValue); + } else { + args[0] = cmd; + String line = String.join(" ", args); + if (StringUtils.isNotBlank(line)) + log.warn("UNKNOWN COMMAND: {}", line); + } + return false; + } + + private void setClusterKeystore(String ksFile, String ksType, String ksPassword, String ksBase64) { + String ksDir = clusterManagerProperties.getTls().getKeystoreDir(); + if (StringUtils.isBlank(ksDir)) ksDir = DEFAULT_KEYSTORE_DIR; + if (!ksDir.endsWith("/")) ksDir += "/"; + this.clusterKeystoreInitialized = true; + this.clusterKeystoreFile = ksDir + ksFile; + this.clusterKeystoreType = ksType; + this.clusterKeystorePassword = ksPassword; + String clusterKeystoreBase64 = ksBase64; + log.info("Cluster Keystore: file: {}", clusterKeystoreFile); + log.info(" type: {}", clusterKeystoreType); + log.info(" password: {}", passwordUtil.encodePassword(clusterKeystorePassword)); + log.debug(" Base64 content: {}", passwordUtil.encodePassword(clusterKeystoreBase64)); + try { + KeystoreUtil + .getKeystore(clusterKeystoreFile, clusterKeystoreType, clusterKeystorePassword) + .passwordUtil(passwordUtil) + .createIfNotExist() + .writeBase64ToFile(clusterKeystoreBase64); + } catch (Exception e) { + log.error("Exception while creating cluster keystore", e); + } + } + + /*protected Properties _base64ToProperties(String paramsStr) { + paramsStr = new String(Base64.getDecoder().decode(paramsStr), StandardCharsets.UTF_8); + //log.trace("params-str: {}", paramsStr); + Properties params = new Properties(); + try { + params.load(new StringReader(paramsStr)); + return params; + } catch (IOException e) { + log.error("Could not deserialize parameters: ", e); + } + return null; + }*/ + + protected synchronized void setClientConfiguration(String configStr) { + try { + log.debug("Received serialization of client configuration: {}", configStr); + ClientConfiguration config = (ClientConfiguration) SerializationUtil.deserializeFromString(configStr); + ClientConfiguration oldConfig = clientConfiguration; + if (oldConfig!=null) { + log.debug("Old client config.: {}", oldConfig); + } + synchronized (groupings) { + clientConfiguration = config; + } + log.info("New client config.: {}", config); + HashMap payload = new HashMap<>(); + payload.put("new", clientConfiguration); + payload.put("old", oldConfig); + eventBus.send(EventConstant.EVENT_CLIENT_CONFIG_UPDATED, payload, this); + + } catch (Exception ex) { + log.error("Exception while deserializing received Client configuration: ", ex); + } + } + + protected synchronized void setGroupingConfiguration(String configStr) { + try { + log.debug("Received serialization of Grouping configuration: {}", configStr); + GroupingConfiguration grouping = (GroupingConfiguration) SerializationUtil.deserializeFromString(configStr); + GroupingConfiguration oldGrouping = groupings.get(grouping.getName()); + if (oldGrouping!=null) { + log.debug("Old grouping config.: {}", oldGrouping); + } + synchronized (groupings) { + groupings.put(grouping.getName(), grouping); + } + log.debug("New grouping config.: {}", grouping); + + } catch (Exception ex) { + log.error("Exception while deserializing received Grouping configuration: ", ex); + } + } + + protected synchronized void setConstants(String configStr) { + try { + log.debug("Received serialization of Constants: {}", configStr); + Map all = StrUtil.castToMapStringObject( + SerializationUtil.deserializeFromString(configStr)); + Map constants = StrUtil.castToMapStringObject(all.get("constants")) + .entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, y -> (Double) y.getValue() + )); + log.debug("Received Constants: {}", constants); + + if (activeGrouping != null) { + log.info("SETTING CONSTANTS: {}", constants); + activeGrouping.setConstants(constants); + brokerCepService.setConstants(constants); + log.debug("New constants set: {}", constants); + } else { + log.warn("No active grouping. Constants will be ignored"); + } + + } catch (Exception ex) { + log.error("Exception while deserializing received Constants: ", ex); + } + } + + protected synchronized void clearGroupings() { + // Clear state of all groupings + log.info("Old active grouping: {}", activeGrouping!=null ? activeGrouping.getName(): null); + log.info("Clearing all groupings..."); + activeGrouping = null; + brokerCepService.clearState(); + groupingsSubscribers.clear(); + log.info("Clearing all groupings completed"); + } + + protected synchronized void setActiveGrouping(String newGroupingName) { + // Checking if new grouping is valid + GroupingConfiguration newGrouping = groupings.get(newGroupingName); + if (newGrouping == null) { + log.error("setActiveGrouping: Grouping specified does not exist: {}", newGroupingName); + return; + } + if ("GLOBAL".equalsIgnoreCase(newGroupingName)) { + throw new IllegalArgumentException("BUG: GLOBAL grouping configuration must have never been set"); + } + + // Figure out if we need to add or remove groupings + boolean addGroupings = true; + String activeGroupingName = "()"; + if (activeGrouping != null) { + activeGroupingName = activeGrouping.getName(); + int diff = GROUPING.valueOf(activeGroupingName).compareTo(GROUPING.valueOf(newGroupingName)); + log.trace("setActiveGrouping: Grouping difference: {}", diff); + if (diff == 0) { + log.info("No need to switch grouping. Active grouping is: {}", newGroupingName); + return; + } + addGroupings = diff > 0; + } + + // Add or Remove groupings between active and new grouping + if (addGroupings) { + log.info("Need to add groupings from {} to {}", activeGroupingName, newGroupingName); + addGroupingsTill(newGroupingName); + } else { + log.info("Need to remove groupings from {} to {}", activeGroupingName, newGroupingName); + removeGroupingsTill(newGroupingName); + } + + // Complete active grouping switch + activeGrouping = groupings.get(newGroupingName); + log.info("Active grouping switch completed: {} -> {}", activeGroupingName, newGroupingName); + String oldGroupingName = activeGroupingName; + activeGroupingName = newGroupingName; + + // Notify Baguette Server about grouping change + log.info("NOTIFY-GROUPING-CHANGE: {}", newGroupingName); + out.println("-NOTIFY-GROUPING-CHANGE: "+newGroupingName); + + // If Aggregator notify Baguette Server + if (clusterManager!=null && GROUPING.valueOf(aggregatorGrouping)==GROUPING.valueOf(newGroupingName)) { + log.info("Notifying Baguette Server i am the new aggregator"); + out.println("CLUSTER AGGREGATOR "+clientId); + } + + // Notify collectors for the active grouping change + final String finalActiveGroupingName = activeGroupingName; + baguetteClient.getCollectorsList() + .forEach(c -> c.activeGroupingChanged(oldGroupingName, finalActiveGroupingName)); + } + + protected synchronized void addGroupingsTill(String newGroupingName) { + // Get available grouping names (in reverse order, i.e. from PER_INSTANCE to PER_CLOUD) + List availableGroupings = GROUPING.getNames().stream() + .filter(groupings::containsKey).collect(Collectors.toList()); + Collections.reverse(availableGroupings); + log.info("addGroupingsTill: Available grouping configurations: {}", availableGroupings); + + // Get groupings between active and new grouping + int start = 0; + if (activeGrouping != null) { + start = availableGroupings.indexOf(activeGrouping.getName()) + 1; + log.trace("addGroupingsTill: active-grouping-index + 1: {}", start); + } + int end = availableGroupings.indexOf(newGroupingName)+1; + log.trace("addGroupingsTill: new-grouping-index + 1: {}", end); + log.trace("addGroupingsTill: grouping-range: [{}..{})", start, end); + List groupingsToAdd = availableGroupings.subList(start, end); + log.debug("addGroupingsTill: groupings-to-add: {}",groupingsToAdd); + + // Collect and merge settings of groupings between active and new + Set eventTypes = new LinkedHashSet<>(); + Map constants = new HashMap<>(); + Set functionDefinitions = new LinkedHashSet<>(); + Map>> rules = new LinkedHashMap<>(); + for (String groupingName : groupingsToAdd) { + log.debug("addGroupingsTill: Merging settings of grouping: {}", groupingName); + GroupingConfiguration grouping = groupings.get(groupingName); + + // Add event types + Set et = grouping.getEventTypeNames(); + eventTypes.addAll(et); + log.trace("addGroupingsTill: + Grouping event types: {}", et); + // Add constants + Map con = grouping.getConstants(); + constants.putAll(con); + log.trace("addGroupingsTill: + Grouping constants: {}", con); + // Add function definitions + Set fd = grouping.getFunctionDefinitions(); + functionDefinitions.addAll(fd); + log.trace("addGroupingsTill: + Grouping func. defs: {}", fd); + // List cep rules + Map> rl = grouping.getRules(); + rules.put(groupingName, rl); + log.trace("addGroupingsTill: + Grouping rule map: {}", rl); + } + log.debug("addGroupingsTill: = Collected event types: {}", eventTypes); + log.debug("addGroupingsTill: = Collected constants: {}", constants); + log.debug("addGroupingsTill: = Collected func. defs: {}", functionDefinitions); + log.debug("addGroupingsTill: = Collected rule maps: {}", rules); + + // Apply merged settings + brokerCepService.addEventTypes(eventTypes, EventMap.getPropertyNames(), EventMap.getPropertyClasses()); + brokerCepService.setConstants(constants); + brokerCepService.addFunctionDefinitions(functionDefinitions); + + // Apply rules-per-topic of new grouping + rules.forEach((groupingName, grpRules) -> { + log.debug("addGroupingsTill: Processing rule map: {}", grpRules); + if (grpRules != null) { + for (Map.Entry> topicRules : grpRules.entrySet()) { + String topic = topicRules.getKey(); + log.info("addGroupingsTill: Processing settings of topic: {}", topic); + for (String rule : topicRules.getValue()) { + // Add EPL statement subscriber + String subscriberName = "Subscriber_" + subscriberCount.getAndIncrement(); + log.info("addGroupingsTill: + Adding subscriber for EPL statement: subscriber-name={}, topic={}, rule={}", subscriberName, topic, rule); + BrokerCepStatementSubscriber statementSubscriber = + new BrokerCepStatementSubscriber(subscriberName, topic, rule, brokerCepService, passwordUtil, Collections.emptySet()); + brokerCepService.getCepService().addStatementSubscriber( + statementSubscriber + ); + groupingsSubscribers.computeIfAbsent(groupingName, s -> new LinkedList<>()).add(statementSubscriber); + } + log.trace("addGroupingsTill: Added to groupingsSubscribers: {}", groupingsSubscribers); + } + } + }); + log.trace("addGroupingsTill: Final groupingsSubscribers: {}", groupingsSubscribers); + + // Clear forward-to-groupings settings of (old) active grouping + clearActiveGroupingForwards(); + + // Set forward-to-topic settings of new grouping (active to-be) + setGroupingForwards(newGroupingName); + + // Update truststore certificates from grouping settings + updateCertificates(groupings.get(newGroupingName)); + } + + protected synchronized void removeGroupingsTill(String newGroupingName) { + // Get available grouping names (in normal order, i.e. from PER_CLOUD to PER_INSTANCE) + List availableGroupings = GROUPING.getNames().stream() + .filter(groupings::containsKey).collect(Collectors.toList()); + log.info("removeGroupingsTill: Available grouping configurations: {}", availableGroupings); + + // Get groupings between active and new grouping + int start = availableGroupings.indexOf(activeGrouping.getName()); + log.trace("removeGroupingsTill: active-grouping-index: {}", start); + + int end = availableGroupings.indexOf(newGroupingName); + log.trace("removeGroupingsTill: new-grouping-index: {}", end); + log.trace("removeGroupingsTill: grouping-range: [{}..{})", start, end); + List groupingsToRemove = availableGroupings.subList(start, end); + log.debug("removeGroupingsTill: groupings-to-remove: {}",groupingsToRemove); + + // Remove subscribers and topics of groupings higher than new grouping + LinkedHashSet eventTypes = new LinkedHashSet<>(); + final CepService cepService = brokerCepService.getCepService(); + for (String groupingName : groupingsToRemove) { + log.debug("removeGroupingsTill: Clearing settings of grouping: {}", groupingName); + GroupingConfiguration grouping = groupings.get(groupingName); + eventTypes.addAll(grouping.getEventTypeNames()); + groupingsSubscribers.get(groupingName).forEach(cepService::removeStatementSubscriber); + groupingsSubscribers.remove(groupingName); + } + eventTypes.forEach(s->brokerCepService.getBrokerCepBridge().removeConsumerOf(s)); + + // Clear forward-to-topic settings of (old) active grouping + clearActiveGroupingForwards(); + + // Set forward-to-topic settings of new grouping (active to-be) + setGroupingForwards(newGroupingName); + } + + private void clearActiveGroupingForwards() { + if (activeGrouping==null) { + log.debug("clearActiveGroupingForwards: No active grouping"); + return; + } + log.debug("clearActiveGroupingForwards: Clearing forward-to-grouping settings of active grouping: {}", activeGrouping.getName()); + log.trace("clearActiveGroupingForwards: Clearing groupingsSubscribers: BEFORE: {}", groupingsSubscribers); + List subscribers = groupingsSubscribers.get(activeGrouping.getName()); + log.trace("clearActiveGroupingForwards: Clearing subscribers of grouping: {}: {}", activeGrouping.getName(), subscribers); + if (subscribers!=null) { + for (BrokerCepStatementSubscriber subscriber : subscribers) { + log.debug("clearActiveGroupingForwards: - Clearing forward-to-grouping settings for: subscriber={}, topic={}, forwards={}", + subscriber.getName(), subscriber.getTopic(), subscriber.getForwardToGroupings()); + subscriber.setForwardToGroupings(Collections.emptySet()); + } + } + log.trace("clearActiveGroupingForwards: Clearing groupingsSubscribers: AFTER: {}", groupingsSubscribers); + } + + private void setGroupingForwards(String newGroupingName) { + GroupingConfiguration newGrouping = groupings.get(newGroupingName); + final Map> topicFwdUrls = new HashMap<>(); + for (Map.Entry> topicRules : newGrouping.getRules().entrySet()) { + String topic = topicRules.getKey(); + log.info("setGroupingForwards: Processing settings of topic: {}", topic); + + // Build forward-to-groupings set for current topic + Set forwardToGroupings = new HashSet<>(); + Set connections = newGrouping.getConnections().get(topic); + log.info("setGroupingForwards: + Adding connections for topic: {} --> {}", topic, connections); + if (connections != null) { + for (String fwdToGrouping : connections) { + BrokerConnectionConfig fwdBrokerConn = newGrouping.getBrokerConnections().get(fwdToGrouping); + forwardToGroupings.add(fwdBrokerConn); + } + } + log.info("setGroupingForwards: = forwardToGroupings of topic {}: {}", topic, forwardToGroupings); + topicFwdUrls.put(topic, forwardToGroupings); + } + log.trace("setGroupingForwards: Update groupingsSubscribers: BEFORE: {}", groupingsSubscribers); + groupingsSubscribers.get(newGroupingName).forEach(subscriber -> { + Set fwdUrls = topicFwdUrls.get(subscriber.getTopic()); + if (fwdUrls!=null) subscriber.setForwardToGroupings(fwdUrls); + }); + log.trace("setGroupingForwards: Update groupingsSubscribers: AFTER: {}", groupingsSubscribers); + } + + protected void updateCertificates(@NonNull GroupingConfiguration grouping) { + if (brokerCepService.getBrokerTruststore()==null) { + log.warn("Broker-CEP trust store has not been initialized. Probably SSL is disabled."); + log.debug("Broker URL: {}", brokerCepService.getBrokerCepProperties().getBrokerUrl()); + return; + } + + // Update truststore with per-grouping broker certificates + try { + log.debug("Truststore certificates before update: {}", + KeystoreUtil.getCertificateAliases(brokerCepService.getBrokerTruststore())); + for (String g : GROUPING.getNames()) { + BrokerConnectionConfig groupingBrokerCfg = grouping.getBrokerConnections().get(g); + if (groupingBrokerCfg != null) { + String brokerUrl = groupingBrokerCfg.getUrl().trim(); + String brokerCert = groupingBrokerCfg.getCertificate().trim(); + String host = null; + if (StringUtils.isNotBlank(brokerUrl)) + host = StringUtils.substringBetween(brokerUrl.trim(), "://", ":"); + log.debug("Grouping host: {}", host); + if (StringUtils.isNotEmpty(brokerCert)) { + //log.debug("Updating broker certificate to truststore for Grouping: {}", g); + //brokerCepService.addOrReplaceCertificateInTruststore(g, brokerCert); + log.debug("Updating broker certificate to truststore for Grouping Host: {}", host); + brokerCepService.addOrReplaceCertificateInTruststore(host, brokerCert); + } else { + log.warn("No broker PEM certificate provided for Grouping: {}", g); + } + } else { + log.debug("Removing broker certificate from truststore for Grouping (no new certificate provided): {}", g); + brokerCepService.deleteCertificateFromTruststore(g); + } + } + log.debug("Truststore certificates after update: {}", + KeystoreUtil.getCertificateAliases(brokerCepService.getBrokerTruststore())); + } catch (Exception ex) { + log.error("EXCEPTION while updating Trust store: ", ex); + } + } + + public void sendLocalEvent(String destination, double metricValue) { + if (activeGrouping != null) { + String brokerUrl = brokerCepService.getBrokerCepProperties().getBrokerUrlForConsumer(); + log.debug("sendLocalEvent(): local-broker-url={}", brokerUrl); + sendEvent(brokerUrl, destination, metricValue); + } else { + log.warn("sendLocalEvent(): No active grouping"); + } + } + + public void sendEvent(String connectionStr, String destination, double metricValue) { + Map event = new HashMap<>(); + event.put("metricValue", metricValue); + event.put("level", 1); + event.put("timestamp", System.currentTimeMillis()); + sendEvent(connectionStr, destination, event); + } + + public CollectorContext.PUBLISH_RESULT sendEvent(String connectionStr, String destination, Map event, boolean createDestination) { + if (log.isTraceEnabled()) + log.trace("sendEvent(): connection-string={}, destination={}, create-destination={}, destination-exists={}, event={}", + connectionStr, destination, createDestination, brokerCepService.destinationExists(destination), event); + CollectorContext.PUBLISH_RESULT result; + if (createDestination || brokerCepService.destinationExists(destination)) { + result = sendEvent(connectionStr, destination, event); + log.trace("sendEvent(): Event sent: destination={}, result={}, event={}", destination, result, event); + return result; + } + result = CollectorContext.PUBLISH_RESULT.SKIPPED; + log.trace("sendEvent(): Event skipped: destination={}, result={}, event={}", destination, result, event); + return result; + } + + public CollectorContext.PUBLISH_RESULT sendEvent(String connectionStr, String destination, Map event) { + try { + log.debug("sendEvent(): Sending event: connection={}, destination={}, event={}", connectionStr, destination, event); + brokerCepService.publishEvent(connectionStr, destination, event); + log.debug("sendEvent(): Event sent: connection={}, destination={}, event={}", connectionStr, destination, event); + return CollectorContext.PUBLISH_RESULT.SENT; + } catch (Exception ex) { + log.error("sendEvent(): Error while sending event: connection={}, destination={}, event={}, exception: ", connectionStr, destination, event, ex); + return CollectorContext.PUBLISH_RESULT.ERROR; + } + } + + protected synchronized String loadCachedClientId() { + // Get the 'cached client id' file name + if (idFile == null) + idFile = DEFAULT_ID_FILE; + + // Check if the cached client id file exists + File file = Paths.get(idFile).toFile(); + if (! file.exists()) { log.warn("loadCachedClientId: Cached client id file not exists: {}", idFile); return null; } + if (! file.isFile()) { log.warn("loadCachedClientId: Cached client id file is not a regular file: {}", idFile); return null; } + + // Load contents of existing 'client id' file + try (InputStream in = new FileInputStream(idFile)) { + + Properties p = new Properties(); + p.load(in); + + // Get cached client id (if any) + String id = p.getProperty("client.id", null); + if (StringUtils.isNotBlank(id)) { + id = id.trim(); + log.info("loadCachedClientId: Used cached Client Id: {}", clientId); + return id; + } else { + log.warn("loadCachedClientId: No cached Client id found in file: {}", idFile); + } + } catch (Exception e) { + log.warn("loadCachedClientId: EXCEPTION while loading cached Client id from file: {}\n", idFile, e); + } + return null; + } + + protected synchronized void saveClientId(String id) { + // Check new id value + if (StringUtils.isBlank(id)) { + log.error("SET-ID: ERROR: Empty id: {}", id); + err.println("ERROR Empty id: " + id); + return; + } + clientId = id.trim(); + + // Load contents of existing 'id file' (if any) + if (StringUtils.isBlank(idFile)) + idFile = DEFAULT_ID_FILE; + Properties p = new Properties(); + // Check if the cached client id file exists + File file = Paths.get(idFile).toFile(); + if (file.exists() && file.isFile()) { + try (InputStream in = new FileInputStream(idFile)) { + p.load(in); + } catch (Exception e) { + log.warn("saveClientId: EXCEPTION while reading cached Client id from file: {}\n", idFile, e); + } + } else { + log.warn("saveClientId: Cached client id file not exists or is not a regular file: {}", idFile); + } + + // Update 'id' in file contents in-memory + p.setProperty("client.id", id); + + // Store new contents into 'id file' + try (OutputStream os = new FileOutputStream(idFile)) { + p.store(os, null); + log.info("ID SET to: {}", id); + if (out!=null) out.println("ID SET"); + } catch (Exception ex) { + log.error("SET-ID: EXCEPTION: ", ex); + err.println("ERROR While storing id to file: " + ex); + } + } + + private BrokerConnectionConfig getBrokerConfiguration() { + BrokerConnectionConfig config = new BrokerConnectionConfig( + activeGrouping!=null ? activeGrouping.getName() : null, + brokerCepService.getBrokerCepProperties().getBrokerUrlForClients(), + brokerCepService.getBrokerCertificate(), + brokerCepService.getBrokerUsername(), + brokerCepService.getBrokerPassword() + ); + log.debug("getBrokerConfiguration: {}", config); + return config; + } + + @SneakyThrows + private String getBrokerConfigurationAsString() { + ObjectMapper mapper = new ObjectMapper(); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + mapper.writer().writeValue(baos, getBrokerConfiguration()); + String configStr = Base64.getEncoder().encodeToString(baos.toByteArray()); + log.debug("getBrokerConfigurationAsString: {}", configStr); + return configStr; + } + } + + @SneakyThrows + private BrokerConnectionConfig getBrokerConfigurationFromString(String configStr) { + log.debug("getBrokerConfigurationFromString: INPUT: {}", configStr); + ObjectMapper mapper = new ObjectMapper(); + BrokerConnectionConfig config = mapper + .readValue(Base64.getDecoder().decode(configStr), BrokerConnectionConfig.class); + log.debug("getBrokerConfigurationFromString: OUTPUT: {}", config); + return config; + } + + private void setBrokerConfigurationFromString(String brokerConfigStr) { + BrokerConnectionConfig brokerConfig = getBrokerConfigurationFromString(brokerConfigStr); + setBrokerConfiguration(brokerConfig); + } + + private void setBrokerConfiguration(BrokerConnectionConfig brokerConfig) { + log.debug("setBrokerConfiguration(): PASSED (NEW) CONFIG:\n{}", brokerConfig); + log.debug("setBrokerConfiguration(): ACTIVE GROUPING: {}", activeGrouping.getName()); + log.debug("setBrokerConfiguration(): OLD BROKER CONNECTIONS:\n{}", activeGrouping.getBrokerConnections()); + + // Update broker connection configuration for aggregator grouping + BrokerConnectionConfig oldConn = activeGrouping.getBrokerConnections().get(aggregatorGrouping); + activeGrouping.getBrokerConnections().put(aggregatorGrouping, brokerConfig); + log.debug("setBrokerConfiguration(): NEW BROKER CONNECTIONS:\n{}", activeGrouping.getBrokerConnections()); + + // Update forward settings of active grouping + // Clear forward-to-groupings settings of active grouping + clearActiveGroupingForwards(); + // Set forward-to-topic settings of active grouping + setGroupingForwards(activeGrouping.getName()); + // Update truststore certificates from active grouping settings + updateCertificates(activeGrouping); + } + + private void nodeStatusChanged(BrokerUtil.NODE_STATUS oldStatus, BrokerUtil.NODE_STATUS newStatus) { + log.info("NOTIFY-STATUS-CHANGE: {}", newStatus.toString()); + out.println("-NOTIFY-STATUS-CHANGE: "+newStatus); + } + + private void sendClientProperty(String propertyName, String propertyValue) { + log.info("CLIENT-PROPERTY-CHANGE: {} = {}", propertyName, propertyValue); + out.printf("-CLIENT-PROPERTY-CHANGE: %s %s%n", propertyName, propertyValue); + } + + @SneakyThrows + private void getStatistics(String inputUuid) { + Map statsMap = brokerCepService.getBrokerCepStatistics(); + log.debug("Statistics: {}", statsMap); + if (out!=null) out.println("-INPUT:"+inputUuid+":"+SerializationUtil.serializeToString(statsMap)); + } + + @SneakyThrows + private void sendStatisticsStart() { + statsSendTask = taskScheduler.scheduleWithFixedDelay(() -> { + try { + Map statsMap = brokerCepService.getBrokerCepStatistics(); + log.debug("BCEP Statistics: {}", statsMap); + Map sysMap = systemResourceMonitor.getLatestMeasurements(); + log.debug("System Statistics: {}", sysMap); + + Map clientStats = new HashMap<>(); + if (statsMap!=null) clientStats.putAll(statsMap); + if (sysMap!=null) clientStats.putAll(sysMap); + if (out != null) out.println("-STATS:" + SerializationUtil.serializeToString(clientStats)); + } catch (Exception ex) { + log.error("Exception while sending Statistics to server: ", ex); + } + }, Duration.ofMillis(baguetteClient.getBaguetteClientProperties().getSendStatisticsDelay())); + log.info("Start sending STATS to server"); + } + + @SneakyThrows + private void sendStatisticsStop() { + statsSendTask.cancel(true); + log.info("Stop sending STATS to server"); + } + + private void clearStatistics() { + brokerCepService.clearBrokerCepStatistics(); + log.info("Statistics cleared"); + if (out!=null) out.println("STATISTICS CLEARED"); + } + + public boolean isAggregator() { + return activeGrouping!=null && aggregatorGrouping!=null && aggregatorGrouping.equals(activeGrouping.getName()); + } + + public boolean isNode() { + return ! isAggregator(); + } + + public void notifyEmsServer(String message) { + log.info("NOTIFY-X: {}", message); + out.println("-NOTIFY-X: "+message); + } + + /*private static class StreamGobbler implements Runnable { + private InputStream inputStream1; + private InputStream inputStream2; + private Consumer consumer; + + public StreamGobbler(InputStream inputStream1, InputStream inputStream2, Consumer consumer) { + this.inputStream1 = inputStream1; + this.inputStream2 = inputStream2; + this.consumer = consumer; + } + + @Override + public void run() { + new BufferedReader(new InputStreamReader(inputStream1)).lines().forEach(consumer); + new BufferedReader(new InputStreamReader(inputStream2)).lines().forEach(consumer); + } + }*/ + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + protected static class ConfigurationContents { + private long timestamp; + private String clientId; + private String activeGrouping; + private Map groupings; + } + + @Data + protected static class ClusterNodeCallback implements BrokerUtil.NodeCallback { + @NonNull private final CommandExecutor commandExecutor; + + private void printInfo(String methodName, String message) { + if (message!=null ) log.debug("{}(): {}", methodName, message); + log.trace("{}(): Node properties: {}", methodName, commandExecutor.getClusterManager().getLocalMemberProperties()); + log.trace("{}(): Back-off flag: {}", methodName, commandExecutor.getClusterManager().getBrokerUtil().isBackOffSet()); + } + + @Override + public void joinedCluster() { + String nodeId = commandExecutor.getClusterManager().getLocalMember().id().id(); + log.info("joinedCluster(): Node joined cluster: {}", nodeId); + commandExecutor.sendClientProperty("node-id", nodeId); + } + + @Override + public void leftCluster() { + log.info("joinedCluster(): Node left cluster"); + commandExecutor.sendClientProperty("node-id", ""); + } + + @Override + public void initialize() { + printInfo("initialize", "INITIALIZE"); + + log.info("initialize(): Node starts initializing as Aggregator..."); + commandExecutor.setActiveGrouping(commandExecutor.getAggregatorGrouping()); + log.info("initialize(): Node initialized as Aggregator"); + } + + @Override + public void stepDown() { + printInfo("stepDown", "STEP DOWN"); + + log.info("stepDown(): Node is Aggregator. Start stepping down..."); + commandExecutor.setActiveGrouping(commandExecutor.getNodeGrouping()); + log.info("stepDown(): Node stepped down"); + } + + @Override + public void statusChanged(BrokerUtil.NODE_STATUS oldStatus, BrokerUtil.NODE_STATUS newStatus) { + log.debug("statusChanged(): Status changed: {} --> {}", oldStatus, newStatus); + commandExecutor.nodeStatusChanged(oldStatus, newStatus); + } + + @Override + public void clusterChanged(ClusterMembershipEvent event) { + log.debug("clusterChanged(): Cluster changed: {} --> {}", event.type(), event.subject().id().id()); + if (commandExecutor.getClusterManager().getBrokerUtil().getLocalStatus()== BrokerUtil.NODE_STATUS.AGGREGATOR) { + if (event.type() == ClusterMembershipEvent.Type.MEMBER_ADDED) { + log.debug("clusterChanged(): Broadcast MEMBER_ADDED in event bus: {}", event.subject().id().id()); + commandExecutor.getEventBus().send(EVENT_CLUSTER_NODE_ADDED, event); + } else + if (event.type() == ClusterMembershipEvent.Type.MEMBER_REMOVED) { + log.debug("clusterChanged(): Broadcast MEMBER_REMOVED in event bus: {}", event.subject().id().id()); + commandExecutor.getEventBus().send(EVENT_CLUSTER_NODE_REMOVED, event); + } + } + } + + @Override + public String getConfiguration(Member local) { + printInfo("getConfiguration", null); + + String brokerConfig = commandExecutor.getBrokerConfigurationAsString(); + log.trace("getConfiguration(): Config. string: {}", brokerConfig); + return brokerConfig; + } + + @Override + public void setConfiguration(String newConfig) { + printInfo("setConfiguration", "SET CONFIG: "+newConfig); + + // Update broker connection configuration for aggregator grouping + commandExecutor.setBrokerConfigurationFromString(newConfig); + } + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/Sshc.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/Sshc.java new file mode 100644 index 0000000..53bbc02 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/Sshc.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client; + +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.simple.SimpleClient; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.mina.MinaServiceFactoryFactory; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.util.io.pem.PemObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.security.PublicKey; +import java.util.Optional; + + +/** + * Custom SSH client + */ +@Slf4j +@Service +public class Sshc implements gr.iccs.imu.ems.common.client.SshClient { + private BaguetteClientProperties config; + private SshClient client; + private SimpleClient simple; + private ClientSession session; + private ClientChannel channel; + private boolean started = false; + @Autowired + private CommandExecutor commandExecutor; + @Autowired + private BrokerCepService brokerCepService; + + @Getter + private InputStream in; + @Getter + private PrintStream out; + @Getter + private PrintStream err; + @Getter + private String clientId; + + @Getter @Setter + private boolean useServerKeyVerifier = true; + + @Override + public void setConfiguration(BaguetteClientProperties config) { + log.trace("Sshc: New config: {}", config); + this.config = config; + this.clientId = config.getClientId(); + log.trace("Sshc: cmd-exec: {}", commandExecutor); + if (this.commandExecutor!=null) this.commandExecutor.setConfiguration(config); + } + + public synchronized void start(boolean retry) throws IOException { + if (retry) { + log.trace("Starting client in retry mode"); + long retryPeriod = config.getRetryPeriod(); + while (!started) { + log.debug("(Re-)trying to start client...."); + try { + start(); + } catch (Exception ex) { + log.warn("{}", ex.getMessage()); + } + if (started) break; + log.trace("Failed to start. Sleeping for {}ms...", retryPeriod); + try { + Thread.sleep(retryPeriod); + } catch (InterruptedException ex) { + log.debug("Sleep: ", ex); + } + } + } else { + start(); + } + if (started) log.trace("Client started"); + } + + @Override + public synchronized void start() throws IOException { + if (started) return; + log.info("Connecting to server..."); + + String host = config.getServerAddress(); + int port = config.getServerPort(); + String serverPubKey = StringEscapeUtils.unescapeJson(config.getServerPubkey()); + String serverPubkeyFingerprint = config.getServerPubkeyFingerprint(); + String serverPubKeyAlgorithm = config.getServerPubkeyAlgorithm(); + String serverPubKeyFormat = config.getServerPubkeyFormat(); + String username = config.getServerUsername(); + String password = config.getServerPassword(); + long connectTimeout = config.getConnectTimeout(); + long authTimeout = config.getAuthTimeout(); + long heartbeatInterval = config.getHeartbeatInterval(); + long heartbeatReplyWait = config.getHeartbeatReplyWait(); + + // Starting client and connecting to server + this.client = SshClient.setUpDefaultClient(); + client.setHostConfigEntryResolver(HostConfigEntryResolver.EMPTY); + + if (useServerKeyVerifier) { + // Get configured server public key + PublicKey pubKey = getPublicKeyFromString(serverPubKeyAlgorithm, serverPubKeyFormat, serverPubKey); + + // Provided server key verifiers + //client.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE); + //client.setServerKeyVerifier(new RequiredServerKeyVerifier(pubKey)); + + // Custom server key verifier + client.setServerKeyVerifier( getCustomServerKeyVerifier(serverPubkeyFingerprint, pubKey) ); + } + + this.simple = SshClient.wrapAsSimpleClient(client); + //simple.setConnectTimeout(connectTimeout); + //simple.setAuthenticationTimeout(authTimeout); + + // Set a huge idle timeout, keep-alive to true and heartbeat to configured value + PropertyResolverUtils.updateProperty(client, CoreModuleProperties.HEARTBEAT_INTERVAL.getName(), heartbeatInterval); // Prevents server-side connection closing + PropertyResolverUtils.updateProperty(client, CoreModuleProperties.HEARTBEAT_REPLY_WAIT.getName(), heartbeatReplyWait); // Prevents client-side connection closing + PropertyResolverUtils.updateProperty(client, CoreModuleProperties.IDLE_TIMEOUT.getName(), Integer.MAX_VALUE); + PropertyResolverUtils.updateProperty(client, CoreModuleProperties.SOCKET_KEEPALIVE.getName(), true); // Socket keep-alive at OS-level + log.debug("Set IDLE_TIMEOUT to MAX, SOCKET-KEEP-ALIVE to true, and HEARTBEAT to {}", heartbeatInterval); + + // Explicitly set IO service factory factory to prevent conflict between MINA and Netty options + client.setIoServiceFactoryFactory(new MinaServiceFactoryFactory()); + + // Start SSH client + client.start(); + + // Authenticate and start session + this.session = client.connect(username, host, port) + .verify(connectTimeout) + .getSession(); + session.addPasswordIdentity(password); + session.auth() + .verify(authTimeout); + + // Open command shell channel + this.channel = session.createChannel(ClientChannel.CHANNEL_SHELL); + PipedInputStream pIn = new PipedInputStream(); + PipedOutputStream pOut = new PipedOutputStream(); + //PipedOutputStream pErr = new PipedOutputStream(); + this.in = new BufferedInputStream(pIn); + this.out = new PrintStream(pOut, true); + //this.err = new PrintStream(pErr, true); + + channel.setIn(new PipedInputStream(pOut)); + channel.setOut(new PipedOutputStream(pIn)); + //channel.setErr(new PipedOutputStream(pErr)); + + channel.open(); + + log.info("SSH client is ready"); + this.started = true; + } + + private static ServerKeyVerifier getCustomServerKeyVerifier(String serverPubkeyFingerprint, PublicKey pubKey) { + return (clientSession, remoteAddress, publicKey) -> { + // boolean verifyServerKey(ClientSession clientSession, SocketAddress socketAddress, PublicKey publicKey) + log.info("verifyServerKey(): remoteAddress: {}", remoteAddress.toString()); + + // Check server public key fingerprint matches with the one in configuration + if (StringUtils.isNoneBlank(serverPubkeyFingerprint)) { + String fingerprint = KeyUtils.getFingerPrint(publicKey); + log.debug("verifyServerKey(): publicKey: fingerprint: {}", fingerprint); + if (fingerprint != null && KeyUtils.checkFingerPrint(serverPubkeyFingerprint, publicKey).getKey() != null) + log.debug("verifyServerKey(): publicKey: fingerprint: MATCH"); + else + log.warn("verifyServerKey(): publicKey: fingerprint: NO MATCH"); + } + + // Check that server public key matches with the one in configuration + try { + // Compare session provided and configured public keys + log.debug("verifyServerKey(): configured server public key: {}", pubKey); + log.debug("verifyServerKey(): received server public key: {}", publicKey); + boolean match = KeyUtils.compareKeys(pubKey, publicKey); + log.debug("verifyServerKey(): Server keys match? {}", match); + return match; + } catch (Exception e) { + log.error("verifyServerKey(): publicKey: EXCEPTION: ", e); + return false; + } + }; + } + + private static PublicKey getPublicKeyFromString(String serverPubKeyAlgorithm, String serverPubKeyFormat, String serverPubKey) throws IOException { + log.debug("getPublicKeyFromString(): serverPubKeyAlgorithm: {}", serverPubKeyAlgorithm); + log.debug("getPublicKeyFromString(): serverPubKeyFormat: {}", serverPubKeyFormat); + log.debug("getPublicKeyFromString(): serverPubKey:\n{}", serverPubKey); + + // Retrieve configured public key - First implementation + PEMParser pemParser = new PEMParser(new StringReader(serverPubKey)); + PemObject pemObject = pemParser.readPemObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemObject.getContent()); + PublicKey pubKey = converter.getPublicKey(publicKeyInfo); + + // Retrieve configured public key - Alternative implementation + /*KeyFactory factory = KeyFactory.getInstance(serverPubKeyAlgorithm); + PublicKey pubKey; + try (StringReader keyReader = new StringReader(serverPubKey); + PemReader pemReader = new PemReader(keyReader)) + { + PemObject pemObject = pemReader.readPemObject(); + byte[] content = pemObject.getContent(); + X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(content); + //or PKCS8EncodedKeySpec pubKeySpec = new PKCS8EncodedKeySpec(content); + pubKey = factory.generatePublic(pubKeySpec); + }*/ + + log.debug("getPublicKeyFromString: Public key: {}", pubKey); + return pubKey; + } + + @Override + public synchronized void stop() throws IOException { + if (!started) return; + this.started = false; + log.info("Stopping SSH client..."); + + channel.close(false).await(); + session.close(false); + simple.close(); + client.stop(); + + log.info("SSH client stopped"); + } + + public synchronized void greeting() { + if (!started) return; + String certOneLine = Optional + .ofNullable(brokerCepService.getBrokerCertificate()) + .orElse("") + .replace(" ","~~") + .replace("\r\n","##") + .replace("\n","$$"); + String clientAddress = config.getDebugFakeIpAddress(); + int clientPort = -1; + out.printf("-HELLO FROM CLIENT: id=%s broker=%s address=%s port=%d username=%s password=%s cert=%s%n", + clientId.replace(" ", "~~"), + brokerCepService.getBrokerCepProperties().getBrokerUrlForClients(), + StringUtils.isNotBlank(clientAddress) ? clientAddress : "", + clientPort, + brokerCepService.getBrokerUsername(), + brokerCepService.getBrokerPassword(), + certOneLine); + out.flush(); + } + + public void run() throws IOException { + if (!started) return; + + // Start communication protocol with Server + // Execution waits here until connection is closed + log.trace("run(): Calling communicateWithServer()..."); + commandExecutor.communicateWithServer(in, out, out); + out.printf("-BYE FROM CLIENT: %s%n", clientId); + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/AbstractLogBase.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/AbstractLogBase.java new file mode 100644 index 0000000..5d09151 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/AbstractLogBase.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.helpers.MessageFormatter; + +import java.io.*; + +@Data +@Slf4j +public abstract class AbstractLogBase { + protected final static Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private BufferedReader rIn = new BufferedReader(new InputStreamReader(System.in)); + private InputStream in = System.in; + private PrintStream out = System.out; + private PrintStream err = System.err; + private boolean logEnabled = true; + private boolean outEnabled = true; + + public void setIn(InputStream in) { this.in = in; this.rIn = new BufferedReader(new InputStreamReader(in)); } + + protected String readLine(String prompt) throws IOException { + out.print(prompt); + out.flush(); + return rIn.readLine(); + } + + protected void log_trace(String formatter, Object...args) { + if (log.isTraceEnabled()) { + if (logEnabled) log.trace(formatter, args); + if (outEnabled) out.println(MessageFormatter.arrayFormat(formatter, args).getMessage()); + } + } + + protected void log_debug(String formatter, Object...args) { + if (log.isDebugEnabled()) { + if (logEnabled) log.debug(formatter, args); + if (outEnabled) out.println(MessageFormatter.arrayFormat(formatter, args).getMessage()); + } + } + + protected void log_info(String formatter, Object...args) { + if (log.isInfoEnabled()) { + if (logEnabled) log.info(formatter, args); + if (outEnabled) out.println(MessageFormatter.arrayFormat(formatter, args).getMessage()); + } + } + + protected void log_warn(String formatter, Object...args) { + if (log.isWarnEnabled()) { + if (logEnabled) log.warn(formatter, args); + if (outEnabled) out.println(MessageFormatter.arrayFormat(formatter, args).getMessage()); + } + } + + protected void log_error(String formatter) { + if (log.isErrorEnabled()) { + if (logEnabled) log.error(formatter); + if (outEnabled) err.println(MessageFormatter.arrayFormat( + formatter, EMPTY_OBJECT_ARRAY, null).getMessage()); + } + } + + protected void log_error(String formatter, Object...args) { + if (log.isErrorEnabled()) { + if (logEnabled) log.error(formatter, args); + if (outEnabled) err.println(MessageFormatter.arrayFormat(formatter, args).getMessage()); + } + } + + protected void log_error(String formatter, Exception ex) { + if (log.isErrorEnabled()) { + if (logEnabled) log.error(formatter, ex); + if (outEnabled) { + err.print(MessageFormatter.arrayFormat( + formatter, EMPTY_OBJECT_ARRAY, ex).getMessage()); + ex.printStackTrace(err); + } + } + } + + protected void out_print(String formatter, Object...args) { stream_print(out, false, formatter, args); } + protected void out_println(String formatter, Object...args) { stream_print(out, true, formatter, args); } + protected void out_println() { stream_print(out, true, "", (Object)null); } + protected void err_print(String formatter, Object...args) { stream_print(err, false, formatter, args); } + protected void err_println(String formatter, Object...args) { stream_print(err, true, formatter, args); } + protected void err_println() { stream_print(err, true, "", (Object)null); } + + protected void stream_print(PrintStream stream, boolean nl, String formatter, Object...args) { + if (outEnabled) { + String message = MessageFormatter.arrayFormat(formatter, args).getMessage(); + if (nl) + stream.println(message); + else + stream.print(message); + stream.flush(); + } + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/BrokerUtil.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/BrokerUtil.java new file mode 100644 index 0000000..0904c19 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/BrokerUtil.java @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.cluster.ClusterMembershipEvent; +import io.atomix.cluster.Member; +import io.atomix.core.Atomix; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import static gr.iccs.imu.ems.baguette.client.cluster.BrokerUtil.NODE_STATUS.*; + +@RequiredArgsConstructor +public class BrokerUtil extends AbstractLogBase { + public enum NODE_STATUS { AGGREGATOR, CANDIDATE, NOT_CANDIDATE, INITIALIZING, STEPPING_DOWN, RETIRING, NOT_SET } + + protected final static Collection BROKER_STATUSES = Arrays.asList(AGGREGATOR, RETIRING); + protected final static Collection CANDIDATE_STATUSES = Arrays.asList(CANDIDATE, AGGREGATOR, INITIALIZING); + protected final static Collection NON_CANDIDATE_STATUSES = Arrays.asList(NOT_CANDIDATE, STEPPING_DOWN, RETIRING, NOT_SET); + + public final static String NODE_MESSAGE_TOPIC = "NODE-MESSAGE-TOPIC"; + public final static String STATUS_PROPERTY = "node-status"; + + protected final static String MESSAGE_ELECTION = "election"; + protected final static String MESSAGE_APPOINT = "appoint"; + protected final static String MESSAGE_INITIALIZE = "initialize"; + protected final static String MESSAGE_READY = "ready"; + private static final String MARKER_NEW_CONFIGURATION = "New config: "; + + private final Atomix atomix; + private final ClusterManager clusterManager; + private final AtomicBoolean backOff = new AtomicBoolean(); + + @Getter @Setter + private NodeCallback callback; + + public BrokerUtil(ClusterManager clusterManager, NodeCallback callback) { + this.clusterManager = clusterManager; + this.atomix = clusterManager.getAtomix(); + this.callback = callback; + } + + void processBrokerMessage(Object m) { + if (m == null) return; + String message = m.toString(); + log_info("BRU: **** Broker message received: {}", message); + + String messageType = message.split(" ", 2)[0]; + if (MESSAGE_ELECTION.equalsIgnoreCase(messageType)) { + // Get excluded nodes (if any) + List excludes = Arrays.stream(message.split(" ")) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .filter(s -> s.startsWith("-")) + .map(s -> s.substring(1)) + .collect(Collectors.toList()); + // Start election + log_info("BRU: **** BROKER: Starting Broker election: "); + election(excludes); + } else if (MESSAGE_APPOINT.equalsIgnoreCase(messageType)) { + String newBrokerId = message.split(" ", 2)[1]; + appointment(newBrokerId); + } else if (MESSAGE_INITIALIZE.equalsIgnoreCase(messageType)) { + String newBrokerId = message.split(" ", 2)[1]; + log_info("BRU: **** BROKER: New Broker initializes: {}", newBrokerId); + // Back off if i am also initializing but have a lower score or command order + backOff(); + } else if (MESSAGE_READY.equalsIgnoreCase(messageType)) { + String[] part = message.split(" ", 3); + String brokerId = part[1]; + String newConfig = part[2]; + // Strip 'New config.' marker + if (newConfig.startsWith(MARKER_NEW_CONFIGURATION)) { + newConfig = newConfig.substring(MARKER_NEW_CONFIGURATION.length()).trim(); + } else { + log_error("BRU: !!!! BUG: New configuration not properly marked: {} !!!!", newConfig); + } + log_info("BRU: **** BROKER: New Broker is ready: {}, New config: {}", brokerId, newConfig); + + // If i am not the new Broker then reset broker status + Member local = getLocalMember(); + NODE_STATUS localStatus = getLocalStatus(); + log_debug("BRU: Nodes: local={}, broker={}", local.id().id(), brokerId); + if (BROKER_STATUSES.contains(localStatus)) + if (!local.id().id().equals(brokerId)) { + // Temporarily make node unavailable for being elected as Broker, until step down completes + setLocalStatus(STEPPING_DOWN); + + // Step down + log_info("BRU: Old broker steps down: {}", local.id().id()); + if (callback!=null) + callback.stepDown(); + + // After step down, and if node hasn't retired, node status changes to 'candidate' + if (RETIRING!=localStatus) + setLocalStatus(CANDIDATE); + else + setLocalStatus(NOT_CANDIDATE); + } + + // Pass new configuration to callback + log_info("BRU: Node configuration updated: {}", newConfig); + if (callback!=null) { + callback.setConfiguration(newConfig); + } + } else + log_warn("BRU: BROKER: Unknown message received: {}", message); + } + + private void aggregatorStepDown() { + // Save previous status + NODE_STATUS oldStatus = getLocalStatus(); + + // Temporarily make node unavailable for being elected as Aggregator, until step down completes + setLocalStatus(STEPPING_DOWN); + + switch (oldStatus) { + case CANDIDATE: + log_debug("BRU: Node is not Aggregator. Clearing back-off flag"); + backOff.set(false); break; + case INITIALIZING: + log_debug("BRU: Node is initializing. Back-off flag set"); + backOff.set(true); break; + case AGGREGATOR: + // Step down + log_info("BRU: Aggregator steps down: {}", getLocalMember().id().id()); + if (callback!=null) + callback.stepDown(); + backOff.set(false); + log_info("BRU: Old aggregator stepped down"); + break; + case STEPPING_DOWN: + log_debug("stepDown(): Node is already stepping down. Nothing to do"); + backOff.set(false); + break; + } + + // After step down, and if node hasn't retired, node status changes to 'candidate' + if (oldStatus!=RETIRING) + setLocalStatus(CANDIDATE); + else + setLocalStatus(NOT_CANDIDATE); + } + + public void backOff() { + NODE_STATUS state = getLocalStatus(); + if (state==INITIALIZING) { + log_debug("BRU: Set Back-off flag to step down after initialization"); + backOff.set(true); + } else + if (state==AGGREGATOR) { + log_debug("BRU: Stepping down because Back-off flag has been set"); + aggregatorStepDown(); + } + } + + public boolean isBackOffSet() { + return backOff.get(); + } + + public void startElection() { + log_info("BRU: Broker election requested: broadcasting election message..."); + atomix.getCommunicationService().broadcastIncludeSelf(NODE_MESSAGE_TOPIC, MESSAGE_ELECTION); + } + + public void election(List excludeNodes) { + // Find the new Brokering node + if (excludeNodes == null) excludeNodes = Collections.emptyList(); + final List excludes = excludeNodes; + Member broker = atomix.getMembershipService().getMembers().stream() + .filter(m -> m.isActive() && m.isReachable()) + .filter(m -> !excludes.contains(m.id().id())) + .filter(m -> CANDIDATE_STATUSES.contains(getNodeStatus(m))) + .map(m -> new MemberWithScore(m, clusterManager.getScoreFunction())) + .peek(ms -> log_info("BRU: Member-Score: {} => {} {}", ms.getMember().id().id(), ms.getScore(), + ms.getMember().properties().getProperty("uuid", null))) + .max(MemberWithScore::compareTo) + .orElse(MemberWithScore.NULL_MEMBER) + .getMember(); + log_info("BRU: Broker: {}", broker != null ? broker.id().id() : null); + + // If local node is the selected broker... + if (getLocalMember().equals(broker)) { + appointment(broker.id().id()); + } + } + + private void appointment(String appointedNodeId) { + // Check i am appointed + Member local = getLocalMember(); + if (! local.id().id().equals(appointedNodeId)) { + log_debug("BRU: I am not appointed: me={} <> appointed={}", local.id().id(), appointedNodeId); + return; + } + + // Check if i am already a broker + NODE_STATUS localStatus = getLocalStatus(); + if (BROKER_STATUSES.contains(localStatus)) { + if (localStatus==RETIRING) { + log_error("BRU: !!!! BUG: RETIRING AGGREGATOR HAS BEEN ELECTED AGAIN !!!!"); + } else { + log_info("BRU: Aggregator elected again"); + } + } else { + // Start initializing as Broker... + aggregatorInitialize(); + } + + // Notify others that this node is ready to serve as Aggregator + String brokerId = local.id().id(); + String newConf = MARKER_NEW_CONFIGURATION + + (callback!=null ? callback.getConfiguration(local) : ""); + atomix.getCommunicationService().broadcastIncludeSelf(NODE_MESSAGE_TOPIC, MESSAGE_READY + " " + brokerId + " " + newConf); + } + + private void aggregatorInitialize() { + if (backOff.getAndSet(false)) { + log_warn("BRU: Node cannot be initialized as Aggregator. Back off flag is set"); + return; + } + + // Notify others that this node starts initializing as Broker + log_info("BRU: Node will become Broker. Initializing..."); + atomix.getCommunicationService().broadcast(NODE_MESSAGE_TOPIC, MESSAGE_INITIALIZE + " " + getLocalMember().id().id()); + setLocalStatus(INITIALIZING); + + // Start initializing as Aggregator... + if (callback!=null) + callback.initialize(); + + // Update node status to Broker + setLocalStatus(AGGREGATOR); + log_info("BRU: Node is ready to act as Aggregator. Ready"); + + if (backOff.getAndSet(false)) { + log_debug("initialize(): Back-off flag has been set. Stepping down immediately."); + aggregatorStepDown(); + } + } + + public void appoint(String brokerId) { + // Check if already a broker + if (getBrokers().stream().anyMatch(m -> m.id().id().equals(brokerId))) { + log_info("BRU: Node is already a broker: {}", brokerId); + if (getNodeStatus(brokerId)==RETIRING) + setNodeStatus(brokerId, AGGREGATOR); + return; + } + + // Check if not a candidate + NODE_STATUS brokerStatus = getNodeStatus(brokerId); + log_debug("BRU: Node status: {}", brokerStatus); + if (NON_CANDIDATE_STATUSES.contains(brokerStatus)) { + log_info("BRU: Node is not a broker candidate: {}", brokerId); + return; + } + + // Broadcast appointment message + atomix.getCommunicationService().broadcastIncludeSelf(NODE_MESSAGE_TOPIC, MESSAGE_APPOINT + " " + brokerId); + log_info("BRU: Broker appointment broadcast: {}", brokerId); + } + + public void retire() { + NODE_STATUS localStatus = getLocalStatus(); + if (BROKER_STATUSES.contains(localStatus)) { + if (localStatus==RETIRING) { + log_info("BRU: Already retiring"); + } else { + setLocalStatus(RETIRING); + log_info("BRU: Broker retires: broadcasting election message..."); + String localNodeId = getLocalMember().id().id(); + atomix.getCommunicationService().broadcast(NODE_MESSAGE_TOPIC, MESSAGE_ELECTION + " -" + localNodeId); + //election(Collections.singletonList(localNodeId)); + } + } else + log_info("BRU: Not an Aggregator"); + } + + public List getBrokers() { + return atomix.getMembershipService().getMembers().stream() + .filter(m -> m.isActive() && m.isReachable()) + .filter(m -> BROKER_STATUSES.contains(getNodeStatus(m))) + .collect(Collectors.toList()); + } + + public Member getLocalMember() { + return atomix.getMembershipService().getLocalMember(); + } + + public NODE_STATUS getLocalStatus() { + return getNodeStatus(getLocalMember()); + } + + public void setLocalStatus(@NonNull NODE_STATUS status) { + setNodeStatus(getLocalMember(), status); + } + + public NODE_STATUS getNodeStatus(@NonNull Member member) { + return NODE_STATUS.valueOf(member.properties().getProperty(STATUS_PROPERTY, NOT_SET.name())); + } + + public void setNodeStatus(@NonNull Member member, @NonNull NODE_STATUS status) { + log_trace("BRU: setNodeStatus: Node properties BEFORE CHANGE: {}", member.properties()); + String oldStatusName = (String) member.properties().setProperty(STATUS_PROPERTY, status.name()); + log_trace("BRU: setNodeStatus: Node properties AFTER CHANGE: {}", member.properties()); + log_debug("BRU: setNodeStatus: Status changed: {} --> {}", oldStatusName, status); + NODE_STATUS oldStatus = StringUtils.isNotBlank(oldStatusName) ? NODE_STATUS.valueOf(oldStatusName) : null; + if (callback!=null & oldStatus!=status) + callback.statusChanged(oldStatus, status); + } + + public NODE_STATUS getNodeStatus(@NonNull String memberId) { + Member member = getMemberById(memberId); + if (member != null) + return getNodeStatus(member); + return null; + } + + public void setNodeStatus(@NonNull String memberId, @NonNull NODE_STATUS status) { + Member member = getMemberById(memberId); + if (member != null) + setNodeStatus(member, status); + } + + private Member getMemberById(@NonNull String id) { + return atomix.getMembershipService().getMembers().stream() + .filter(m -> m.isActive() && m.isReachable()) + .filter(m -> m.id().id().equals(id)) + .findFirst() + .orElse(null); + } + + public void setCandidate() { + NODE_STATUS localStatus = getLocalStatus(); + if (localStatus==NOT_CANDIDATE || localStatus==NOT_SET) { + setLocalStatus(CANDIDATE); + log_info("BRU: Node becomes Aggregator candidate"); + } else + log_info("BRU: Node is already Aggregator candidate"); + } + + public void clearCandidate() { + NODE_STATUS localStatus = getLocalStatus(); + if (BROKER_STATUSES.contains(localStatus)) { + log_warn("BRU: Node is the Aggregator. Select 'retire' first"); + return; + } + if (localStatus==INITIALIZING) { + log_warn("BRU: Node is initializing for Aggregator. Step down first"); + return; + } + if (localStatus==STEPPING_DOWN) { + log_warn("BRU: Node is stepping down. Wait step down complete"); + return; + } + if (localStatus==CANDIDATE) { + setLocalStatus(NOT_CANDIDATE); + log_info("BRU: Node removed from Broker candidates"); + } else + log_info("BRU: Node is not Aggregator candidate"); + } + + public List getCandidates() { + return atomix.getMembershipService().getMembers().stream() + .filter(m -> m.isActive() && m.isReachable()) + .filter(m -> CANDIDATE_STATUSES.contains(getNodeStatus(m))) + .map(m -> new MemberWithScore(m, clusterManager.getScoreFunction())) + .collect(Collectors.toList()); + } + + public List getActiveNodes() { + return atomix.getMembershipService().getMembers().stream() + .filter(m -> m.isActive() && m.isReachable()) + .map(m -> new MemberWithScore(m, clusterManager.getScoreFunction())) + .collect(Collectors.toList()); + } + + public void checkBroker() { + List brokers = getBrokers(); + log_info("BRU: Brokers after cluster change: {}", brokers); + + // Check if any node is initializing as broker (then don't start election) + if (getActiveNodes().stream() + .map(MemberWithScore::getMember) + .map(this::getNodeStatus) + .noneMatch(s -> INITIALIZING==s || AGGREGATOR==s)) + { + startElection(); + } + } + + public void checkBrokerNumber() { + List brokers = getBrokers(); + log_debug("BRU: Check number of Brokers in cluster: {}", brokers); + + // Check if there are more than one brokers in cluster + long numOfBrokers = getActiveNodes().stream() + .map(MemberWithScore::getMember) + .map(this::getNodeStatus) + .filter(s -> AGGREGATOR==s) + .count(); + log_info("BRU: Number of Brokers in cluster: {}", numOfBrokers); + if (numOfBrokers>1) { + log_warn("BRU: {} brokers found in the cluster. Starting election...", numOfBrokers); + startElection(); + } + } + + public interface NodeCallback { + void joinedCluster(); + void leftCluster(); + + void initialize(); + void stepDown(); + void statusChanged(NODE_STATUS oldStatus, NODE_STATUS newStatus); + void clusterChanged(ClusterMembershipEvent event); + String getConfiguration(Member local); + void setConfiguration(String newConfig); + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterCLI.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterCLI.java new file mode 100644 index 0000000..a665861 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterCLI.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.cluster.Member; +import io.atomix.cluster.MemberId; +import io.atomix.cluster.messaging.ClusterCommunicationService; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +@RequiredArgsConstructor +public class ClusterCLI extends AbstractLogBase { + + private final ClusterManager clusterManager; + + @Getter @Setter + private String prompt = " -> "; + @Getter @Setter + private boolean promptUpdate; + + public void updatePrompt() { + if (promptUpdate) { + setPrompt((clusterManager != null && clusterManager.isRunning()) + ? "[" + clusterManager.getLocalMember().id().id() + "] => " + : " => "); + } + } + + public void run() { + run(false, false, false, true); + } + + public void run(boolean joinOnStart, boolean leaveOnExit, boolean autoElect, boolean allowExit) { + if (joinOnStart && !clusterManager.isInitialized()) { + clusterManager.initialize(); + } + if (joinOnStart && !clusterManager.isRunning()) { + clusterManager.joinCluster(autoElect); + } + updatePrompt(); + + // Start doing work... + while (true) { + try { + String line = readLine(prompt); + if (StringUtils.isBlank(line)) continue; + String[] cmd = line.trim().split(" "); + + if ("exit".equalsIgnoreCase(cmd[0])) { + if (allowExit) + break; + } else { + executeCommand(line, cmd); + } + + } catch (Exception ex) { + log_error("CLI: Exception caught: ", ex); + } + } + + if (leaveOnExit && clusterManager.isRunning()) + clusterManager.leaveCluster(); + } + + public void executeCommand(String line, String[] cmd) { + if ("properties".equalsIgnoreCase(cmd[0])) { + Properties properties = clusterManager.getLocalMember().properties(); + log_info("CLI: Local member properties:"); + for (String propName : properties.stringPropertyNames()) { + log_info("CLI: {} = {}", propName, properties.getProperty(propName)); + } + } else if ("set".equalsIgnoreCase(cmd[0])) { + String setStr = line.trim().split(" ", 2)[1]; + int p = setStr.indexOf("="); + String propName = setStr.substring(0, p).trim(); + String propValue = setStr.substring(p + 1).trim(); + log_info("CLI: SET PROPERTY: {} = {}", propName, propValue); + clusterManager.getLocalMember().properties().setProperty(propName, propValue); + } else if ("unset".equalsIgnoreCase(cmd[0])) { + String propName = cmd[1].trim(); + log_info("CLI: UNSET PROPERTY: {}", propName); + clusterManager.getLocalMember().properties().setProperty(propName, ""); + } else if ("score".equalsIgnoreCase(cmd[0])) { + if (cmd.length==1) { + log_info("CLI: Score function: {}", clusterManager.getScoreFunction()); + } else { + String formula = clusterManager.getScoreFunction().getFormula(); + Properties defs = new Properties(); + defs.putAll(clusterManager.getScoreFunction().getArgumentDefaults()); + double defScore = clusterManager.getScoreFunction().getDefaultScore(); + boolean throwExceptions = clusterManager.getScoreFunction().isThrowExceptions(); + if (!"-".equals(cmd[1]) && !"same".equalsIgnoreCase(cmd[1])) + formula = cmd[1]; + for (int i = 2; i < cmd.length; i++) { + String[] part = cmd[i].split("=", 2); + if ("default".equalsIgnoreCase(part[0])) { + throwExceptions = false; + if ("-".equals(part[1])) + throwExceptions = true; + else + defScore = Double.parseDouble(part[1]); + } else if ("clear-defaults".equalsIgnoreCase(part[0])) + defs.clear(); + else + defs.setProperty(part[0], String.valueOf(Double.parseDouble(part[1]))); + } + clusterManager.setScoreFunction(MemberScoreFunction.builder() + .formula(formula) + .argumentDefaults(defs) + .defaultScore(defScore) + .throwExceptions(throwExceptions) + .build()); + } + + } else if ("members".equalsIgnoreCase(cmd[0])) { + // Get cluster members + log_info("CLI: Cluster members:"); + for (Member member : clusterManager.getMembers()) { + String memId = member.id().id(); + String memAddress = member.config().getAddress().toString(); + Set> memProperties = member.properties().entrySet(); + String active = (member.isActive() ? "active" : "inactive"); + String reachable = (member.isReachable() ? "reachable" : "unreachable"); + log_info("CLI: {}/{}/{}-{}/{}", memId, memAddress, active, reachable, memProperties); + } + } else if ("join".equalsIgnoreCase(cmd[0])) { + if (cmd.length>1) { + ArrayList tmp = new ArrayList<>(Arrays.asList(cmd)); + tmp.remove(0); + clusterManager.getProperties().setMemberAddresses(tmp); + } + + // Join/start cluster + clusterManager.initialize(); + clusterManager.joinCluster(); + updatePrompt(); + + } else if ("leave".equalsIgnoreCase(cmd[0])) { + clusterManager.leaveCluster(); + updatePrompt(); + + } else if ("message".equalsIgnoreCase(cmd[0])) { + ClusterCommunicationService communicationService = clusterManager.getAtomix().getCommunicationService(); + String op = cmd[1]; + String topic = cmd[2]; + if ("subscribe".equalsIgnoreCase(op)) { + communicationService.subscribe(topic, (m) -> { + log_info("CLI: **** Message: {} on Topic: {}", m, topic); + return CompletableFuture.completedFuture("Ok"); + }).join(); + log_info("CLI: Subscribed to topic: {}", topic); + } else + if ("unsubscribe".equalsIgnoreCase(op)) { + log_info("CLI: Unsubscribe from topic: {}", topic); + communicationService.unsubscribe(topic); + } else + if ("broadcast".equalsIgnoreCase(op)) { + log_info("CLI: Broadcast to topic: {}", topic); + String message = String.join(" ", Arrays.copyOfRange(cmd, 3, cmd.length)); + communicationService.broadcast(topic, message); + } else + if ("send".equalsIgnoreCase(op)) { + MemberId mId = MemberId.from(cmd[3]); + log_info("CLI: Send to node: {}, on topic: {}", cmd[3], topic); + String message = String.join(" ", Arrays.copyOfRange(cmd, 4, cmd.length)); + communicationService.send(topic, message, mId).join(); + } else + if ("unicast".equalsIgnoreCase(op)) { + MemberId mId = MemberId.from(cmd[3]); + log_info("CLI: Send to node: {}, on topic: {}", cmd[3], topic); + String message = String.join(" ", Arrays.copyOfRange(cmd, 3, cmd.length)); + communicationService.unicast(topic, message, mId).join(); + } else + log_warn("CLI: Invalid Message operation: {}", op); + + } else if ("broker".equalsIgnoreCase(cmd[0]) || "bl".equalsIgnoreCase(cmd[0])) { + String op = cmd.length>1 ? cmd[1] : null; + if ("list".equalsIgnoreCase(op) || "bl".equalsIgnoreCase(cmd[0])) { + log_info("CLI: Node status and scores:"); + final BrokerUtil brokerUtil1 = clusterManager.getBrokerUtil(); + brokerUtil1.getActiveNodes().forEach(ms -> log_info("CLI: {} [{}, {}, {}]", + ms.getMember().id().id(), brokerUtil1.getNodeStatus(ms.getMember()), + ms.getScore(), ms.getMember().properties().getProperty("uuid", null))); + } else + if ("candidates".equalsIgnoreCase(op)) { + log_info("CLI: Broker candidates:"); + final BrokerUtil brokerUtil1 = clusterManager.getBrokerUtil(); + brokerUtil1.getCandidates().forEach(ms -> log_info("CLI: {} [{}, {}, {}]", + ms.getMember().id().id(), brokerUtil1.getNodeStatus(ms.getMember()), + ms.getScore(), ms.getMember().properties().getProperty("uuid", null))); + } else + if ("status".equalsIgnoreCase(op)) { + clusterManager.getBrokerUtil().getBrokers() + .forEach(m -> log_info("CLI: Current Broker: {}", m.id().id())); + } else + if ("elect".equalsIgnoreCase(op)) { + clusterManager.getBrokerUtil().startElection(); + } else + if ("retire".equalsIgnoreCase(op)) { + clusterManager.getBrokerUtil().retire(); + } else + if ("appoint".equalsIgnoreCase(op)) { + clusterManager.getBrokerUtil().appoint(cmd[2]); + } else + if ("on".equalsIgnoreCase(op)) { + clusterManager.getBrokerUtil().setCandidate(); + } else + if ("off".equalsIgnoreCase(op)) { + clusterManager.getBrokerUtil().clearCandidate(); + } else + log_warn("CLI: Invalid Broker operation: {}", op); + + } else + log_warn("CLI: Unknown command: {}", cmd[0]); + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterManager.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterManager.java new file mode 100644 index 0000000..d0fcdce --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterManager.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.cluster.ClusterMembershipEvent; +import io.atomix.cluster.Member; +import io.atomix.cluster.MemberId; +import io.atomix.cluster.Node; +import io.atomix.cluster.discovery.BootstrapDiscoveryProvider; +import io.atomix.cluster.discovery.NodeDiscoveryProvider; +import io.atomix.cluster.protocol.GroupMembershipProtocol; +import io.atomix.cluster.protocol.HeartbeatMembershipProtocol; +import io.atomix.cluster.protocol.SwimMembershipProtocol; +import io.atomix.core.Atomix; +import io.atomix.core.AtomixBuilder; +import io.atomix.protocols.backup.partition.PrimaryBackupPartitionGroup; +import io.atomix.protocols.raft.partition.RaftPartitionGroup; +import io.atomix.utils.net.Address; +import lombok.*; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.stream.Collectors; + +@Data +@Component +@EqualsAndHashCode(callSuper = true) +public class ClusterManager extends AbstractLogBase { + + private static final String NODE_NAME_PREFIX = "node_"; + + private ClusterManagerProperties properties; + private BrokerUtil.NodeCallback callback; + private ClusterCLI cli; + + private MemberScoreFunction scoreFunction = new MemberScoreFunction("-1"); + + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private Address localAddress = null; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private NodeDiscoveryProvider bootstrapDiscoveryProvider = null; + @Setter(AccessLevel.NONE) + private Atomix atomix = null; + @Setter(AccessLevel.NONE) + private BrokerUtil brokerUtil = null; + + @Autowired + private TaskScheduler taskScheduler; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private ScheduledFuture checkerTask; + + // ------------------------------------------------------------------------ + + public synchronized ClusterCLI getCli() { + if (cli==null) { + cli = new ClusterCLI(this); + cli.setLogEnabled(isLogEnabled()); + cli.setOutEnabled(isOutEnabled()); + } + return cli; + } + + public Atomix getAtomix() { + if (atomix==null) throw new IllegalStateException("Not initialized"); + return atomix; + } + + public BrokerUtil getBrokerUtil() { + if (brokerUtil==null) throw new IllegalStateException("Not initialized"); + return brokerUtil; + } + + public Set getMembers() { + return getAtomix().getMembershipService().getMembers(); + } + + public Member getLocalMember() { + return getAtomix().getMembershipService().getLocalMember(); + } + + public Address getLocalAddress() { + return getLocalMember().address(); + } + + public Properties getLocalMemberProperties() { + return getAtomix().getMembershipService().getLocalMember().properties(); + } + + public void setCallback(BrokerUtil.NodeCallback callback) { + this.callback = callback; + if (brokerUtil!=null) brokerUtil.setCallback(callback); + } + + // ------------------------------------------------------------------------ + + public boolean isInitialized() { + return atomix!=null; + } + + public boolean isRunning() { + return (atomix!=null && atomix.isRunning()); + } + + public void initialize() { + initialize(properties, callback); + } + + public void initialize(ClusterManagerProperties p) { + initialize(p, this.callback); + } + + public void initialize(ClusterManagerProperties p, BrokerUtil.NodeCallback callback) { + // Store properties and callback + if (p!=null) this.properties = p; + if (callback!=null) this.callback = callback; + + // Set logging and output flags + setLogEnabled(properties.isLogEnabled()); + setOutEnabled(properties.isOutEnabled()); + + // Initialize member scoring function + this.scoreFunction = properties.getScore()!=null + ? MemberScoreFunction.builder() + .formula(properties.getScore().getFormula()) + .defaultScore(properties.getScore().getDefaultScore()) + .argumentDefaults(properties.getScore().getDefaultArgs()) + .throwExceptions(properties.getScore().isThrowException()) + .build() + : this.scoreFunction; + + // Get local address and port + localAddress = properties.getLocalNode().getAddress(); + log_debug("CLM: Provided local-address: {}", localAddress); + if (localAddress==null) { + //localAddress = Address.from(getLocalHostName() + ":1234"); + localAddress = Address.from(getLocalHostAddress() + ":1234"); + log_debug("CLM: Resolving local-address: {}", localAddress); + } + log_info("CLM: Local address used for building Atomix: {}", localAddress); + + // Initialize Membership provider + bootstrapDiscoveryProvider = buildNodeDiscoveryProvider(properties.getMemberAddresses()); + + // Create Atomix and Join/start cluster + atomix = buildAtomix(properties, localAddress, bootstrapDiscoveryProvider); + brokerUtil = new BrokerUtil(this, callback); + brokerUtil.setLogEnabled(isLogEnabled()); + brokerUtil.setOutEnabled(isOutEnabled()); + } + + public void joinCluster() { + joinCluster(getProperties().isElectionOnJoin()); + } + + public void joinCluster(boolean startElection) { + // Initialize cluster if needed + if (atomix==null) + initialize(); + + // Start/Join cluster + log_info("CLM: Joining cluster..."); + long startTm = System.currentTimeMillis(); + atomix.start().join(); + long endTm = System.currentTimeMillis(); + log_debug("CLM: Joined cluster in {}ms", endTm-startTm); + + // Populate default local member properties + Member localMember = atomix.getMembershipService().getLocalMember(); + String addrStr = localMember.address().host() + ":" + localMember.address().port(); + atomix.getMembershipService().getLocalMember().properties().setProperty("address", addrStr); + atomix.getMembershipService().getLocalMember().properties().setProperty("uuid", UUID.randomUUID().toString()); + brokerUtil.setLocalStatus(BrokerUtil.NODE_STATUS.CANDIDATE); + + // Add membership listener + atomix.getMembershipService().addListener(event -> { + log_debug("CLM: {}: node={}", event.type(), event.subject()); + if (event.type()!=ClusterMembershipEvent.Type.REACHABILITY_CHANGED) { + if (event.type()!=ClusterMembershipEvent.Type.METADATA_CHANGED) { + log_info("CLM: {}: node={}", event.type(), event.subject().id().id()); + brokerUtil.checkBroker(); + } + if (callback!=null) + callback.clusterChanged(event); + } + }); + + // Add broker message listener + atomix.getCommunicationService().subscribe(BrokerUtil.NODE_MESSAGE_TOPIC, m -> { + brokerUtil.processBrokerMessage(m); + return CompletableFuture.completedFuture("ok"); + }); + + // Start election if no broker exists + if (startElection) { + brokerUtil.checkBroker(); + } + + // Start cluster checker + if (properties.isClusterCheckerEnabled()) { + long delay = Math.max(properties.getClusterCheckerDelay(), 10000L); + log_info("CLM: Starting cluster checker (delay: {})...", delay); + checkerTask = taskScheduler.scheduleWithFixedDelay(() -> { + if (brokerUtil != null) + brokerUtil.checkBrokerNumber(); + else + log_warn("CLM: Cluster checker: BrokerUtil is NULL (is it a BUG?)"); + }, Duration.ofMillis(delay)); + } else { + log_warn("CLM: Cluster checker is DISABLED"); + } + } + + public void waitToJoin() { + while (true) { + if (isInitialized() && isRunning()) break; + try { Thread.sleep(500); } catch (InterruptedException e) { break; } + } + if (callback!=null) + callback.joinedCluster(); + } + + public void waitToJoin(long waitForMillis) { + long startTm = System.currentTimeMillis(); + long endTm = startTm + waitForMillis; + while (true) { + if (isInitialized() && isRunning()) break; + long waitFor = Math.min(500, endTm-System.currentTimeMillis()); + try { Thread.sleep(waitFor); } catch (InterruptedException e) { break; } + } + if (callback!=null) + callback.joinedCluster(); + } + + public void leaveCluster() { + // Stop cluster checker + if (checkerTask!=null && !checkerTask.isCancelled()) { + log_info("CLM: Stopping cluster checker..."); + checkerTask.cancel(true); + checkerTask = null; + } + + // Leave cluster + log_info("CLM: Leaving cluster..."); + long startTm = System.currentTimeMillis(); + if (atomix.isRunning()) + atomix.stop().join(); + long endTm = System.currentTimeMillis(); + log_debug("CLM: Left cluster in {}ms", endTm-startTm); + atomix = null; + brokerUtil = null; + if (callback!=null) + callback.leftCluster(); + } + + // ------------------------------------------------------------------------ + + public static String getLocalHostName() { + String hostname = null; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + //log_error("Exception while getting Node hostname: ", e); + } + if (StringUtils.isBlank(hostname)) + hostname = getLocalHostAddress(); + return hostname; + } + + public static String getLocalHostAddress() { + String address = null; + try { + address = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + //log_error("Exception while getting Node local address: ", e); + } + if (StringUtils.isBlank(address)) + address = UUID.randomUUID().toString(); + return address; + } + + // ------------------------------------------------------------------------ + + private String createMemberName(int port) { return createMemberName(getLocalHostName()+":"+port); } + private String createMemberName(String address) { + return NODE_NAME_PREFIX+address.replace(":", "_"); + } + + private Node createNode(String address, String port) { return createNode(address, Integer.parseInt(port)); } + private Node createNode(String address, int port) { return createNode(address+":"+port); } + private Node createNode(String address) { + return Node.builder() + .withId(createMemberName(address)) + .withAddress(Address.from(address)) + .build(); + } + private Node createNode(ClusterManagerProperties.NodeProperties nodeProperties) { + String nodeId = nodeProperties.getId(); + if (StringUtils.isBlank(nodeId)) + nodeId = createMemberName(nodeProperties.getAddress().port()); + return Node.builder() + .withId(nodeId) + .withAddress(nodeProperties.getAddress()) + .build(); + } + + public static Address getAddressFromString(String localAddressStr) { + Address localAddress; + localAddressStr = localAddressStr.trim(); + if (StringUtils.isBlank(localAddressStr)) { + localAddress = Address.local(); + } else + if (StringUtils.isNumeric(localAddressStr)) { + localAddress = Address.from(Integer.parseInt(localAddressStr)); + } else { + localAddress = Address.from(localAddressStr); + } + return localAddress; + } + + private NodeDiscoveryProvider buildNodeDiscoveryProvider(List addresses) { + return buildNodeDiscoveryProviderFromProperties( + addresses!=null + ? addresses.stream() + .map(ClusterManager::getAddressFromString) + .map(address -> new ClusterManagerProperties.NodeProperties(null, address, null)) + .collect(Collectors.toList()) + : null); + } + + private NodeDiscoveryProvider buildNodeDiscoveryProviderFromProperties(List nodePropertiesList) { + List nodes = new ArrayList<>(); + if (nodePropertiesList!=null) { + nodes = nodePropertiesList.stream().map(this::createNode).collect(Collectors.toList()); + } + log_info("CLM: Building Atomix: Other members: {}", nodes); + return BootstrapDiscoveryProvider.builder() + .withNodes(nodes) + //.withHeartbeatInterval(Duration.ofSeconds(5)) + //.withFailureThreshold(2) + //.withFailureTimeout(Duration.ofSeconds(1)) + .build(); + } + + private MemberId[] getMemberIds(Set nodes) { + List memberIdList = new ArrayList<>(); + for (Node node : nodes) + memberIdList.add(MemberId.from(node.id().id())); + return memberIdList.toArray(new MemberId[0]); + } + + private Member[] getMembers(Set nodes) { + List memberList = new ArrayList<>(); + for (Node node : nodes) + memberList.add(Member.builder() + .withId(node.id().id()) + .withAddress(node.address()) + .build()); + return memberList.toArray(new Member[0]); + } + + private Atomix buildAtomix(ClusterManagerProperties properties, Address localAddress, NodeDiscoveryProvider bootstrapDiscoveryProvider) { + // Configuring local cluster member + AtomixBuilder atomixBuilder = Atomix.builder(); + + // Cluster id + String clusterId = properties.getClusterId(); + if (StringUtils.isNotBlank(clusterId)) { + log_info("CLM: Building Atomix: Cluster-id: {}", clusterId); + atomixBuilder.withClusterId(clusterId); + } + + // Local member id and address + String memId = properties.getLocalNode().getId(); + memId = StringUtils.isBlank(memId) ? createMemberName(localAddress.port()) : memId; + MemberId localMemberId = MemberId.from(memId); + log_info("CLM: Building Atomix: Local-Member-Id: {}", localMemberId); + log_info("CLM: Building Atomix: Local-Member-Address: {}", localAddress); + atomixBuilder + .withMemberId(localMemberId) + .withAddress(localAddress) + .withProperties(properties.getLocalNode().getProperties()); + + // Configure membership protocol + boolean useSwim = properties.isUseSwim(); + long failureTimeout = Math.max(100L, properties.getFailureTimeout()); + GroupMembershipProtocol memProto; + atomixBuilder + .withMembershipProtocol(memProto = useSwim + ? SwimMembershipProtocol.builder() + //.withGossipInterval(Duration.ofMillis(250)) + //.withGossipFanout(2) + .withFailureTimeout(Duration.ofMillis(failureTimeout)) + .build() + : HeartbeatMembershipProtocol.builder() + //.withHeartbeatInterval(Duration.ofMillis(1000)) + .withFailureTimeout(Duration.ofMillis(failureTimeout)) + //.withFailureThreshold(2) + .build() + ); + log_info("CLM: Building Atomix: Membership protocol: {}", memProto.getClass().getSimpleName()); + + // Configure Management and Partition groups + boolean usePBInMg = properties.isUsePBInMg(); + boolean usePBInPg = properties.isUsePBInPg(); + String mgName = properties.getMgName(); + String pgName = properties.getPgName(); + if (StringUtils.isBlank(mgName)) mgName = "system"; + if (StringUtils.isBlank(pgName)) pgName = "data"; + log_debug("CLM: Building Atomix: Cluster Groups: mg-type-PB={}, pg-type-PB={}, mg-name={}, pg-name={}", + usePBInMg, usePBInPg, mgName, pgName); + atomixBuilder + .withManagementGroup(usePBInMg + ? PrimaryBackupPartitionGroup.builder(mgName) + .withNumPartitions(1) + //.withMemberGroupStrategy(MemberGroupStrategy.NODE_AWARE) + .build() + : RaftPartitionGroup.builder(mgName) + .withNumPartitions(1) + .withMembers(getMemberIds(bootstrapDiscoveryProvider.getNodes())) + //.withMembers(getMembers(bootstrapDiscoveryProvider.getNodes())) + //.withDataDirectory(new File("raft-mg")) + //.withMemberGroupStrategy(MemberGroupStrategy.NODE_AWARE) + .build() + ) + .withPartitionGroups(usePBInPg + ? PrimaryBackupPartitionGroup.builder(pgName) + .withNumPartitions(8) + //.withMemberGroupStrategy(MemberGroupStrategy.NODE_AWARE) + .build() + : RaftPartitionGroup.builder(pgName) + .withNumPartitions(8) + .withMembers(getMemberIds(bootstrapDiscoveryProvider.getNodes())) + //.withMembers(getMembers(bootstrapDiscoveryProvider.getNodes())) + //.withDataDirectory(new File("raft-pg")) + //.withMemberGroupStrategy(MemberGroupStrategy.NODE_AWARE) + .build() + ); + + // Configure Bootstrap Discovery Provider + atomixBuilder + //.withMulticastEnabled() + .withMembershipProvider(bootstrapDiscoveryProvider); + + // Configure TLS for messaging + log_info("CLM: Building Atomix: TLS enabled={}", properties.getTls().isEnabled()); + if (properties.getTls().isEnabled()) { + atomixBuilder + .withTlsEnabled(true) + .withKeyStore(properties.getTls().getKeystore()) + .withKeyStorePassword(properties.getTls().getKeystorePassword()) + .withTrustStore(properties.getTls().getTruststore()) + .withTrustStorePassword(properties.getTls().getTruststorePassword()); + } + + return atomixBuilder.build(); + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterManagerProperties.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterManagerProperties.java new file mode 100644 index 0000000..be3ee1b --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterManagerProperties.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.utils.net.Address; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Properties; + +@Data +@Configuration +@ConfigurationProperties(prefix = "cluster") +public class ClusterManagerProperties { + private String clusterId = "local-cluster"; + private NodeProperties localNode = new NodeProperties(); + private List memberAddresses; + + private boolean useSwim = true; // ...else the Heartbeat membership protocol will be used + private long failureTimeout = 10000; // The Atomix default failure timeout for both membership protocols + private long testInterval = -1; // Print cluster node status every X millis (negative numbers should turn off feature) + + private boolean logEnabled; + private boolean outEnabled = true; + + private boolean joinOnInit = true; + private boolean electionOnJoin; + + private boolean clusterCheckerEnabled = true; + private long clusterCheckerDelay = 30000L; + + private boolean usePBInMg = true; + private boolean usePBInPg = true; + private String mgName = "system"; + private String pgName = "data"; + + private TlsProperties tls = new TlsProperties(); + + private ScoreFunctionProperties score; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class NodeProperties { + private String id; + private Address address; + private Properties properties = new Properties(); + + public NodeProperties(String address) { + this.address = Address.from(address); + } + + public void setAddress(String address) { + this.address = ClusterManager.getAddressFromString(address); + } + } + + @Data + @ToString(exclude = {"keystorePassword", "truststorePassword"}) + public static class TlsProperties { + private boolean enabled; + private String keystore; + private String keystorePassword; + private String truststore; + private String truststorePassword; + private String keystoreDir; + } + + @Data + public static class ScoreFunctionProperties { + private String formula; + private double defaultScore; + private Properties defaultArgs; + private boolean throwException; + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterTest.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterTest.java new file mode 100644 index 0000000..639bb0b --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/ClusterTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.core.Atomix; +import lombok.*; + +import java.util.stream.Collectors; + +@Data +public class ClusterTest implements Runnable { + + @NonNull + private final ClusterManager clusterManager; + + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private Thread runner; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private boolean keepRunning; + private long delay = 5000; + + public void startTest(long delay) { + checkRunning(); + if (delay < 1) throw new IllegalArgumentException("ClusterTest delay must be positive: " + delay); + this.delay = delay; + startTest(); + } + + public synchronized void startTest() { + checkRunning(); + runner = new Thread(this); + runner.setDaemon(true); + keepRunning = true; + runner.start(); + } + + public synchronized void stopTest() { + checkNotRunning(); + keepRunning = false; + runner.interrupt(); + runner = null; + } + + private void checkRunning() { + if (keepRunning) + throw new IllegalStateException("ClusterTest is already running"); + } + + private void checkNotRunning() { + if (!keepRunning) + throw new IllegalStateException("ClusterTest is not running"); + } + + public void run() { + // Start doing work... + Atomix atomix = clusterManager.getAtomix(); + int iterations = 0; + while (keepRunning) { + iterations++; + clusterManager.log_info("-- Iter={} ---------------------------------------", iterations); + + // Get cluster members + clusterManager.log_info("-- CLUSTER-MEMBERS: {}", atomix.getMembershipService().getMembers().stream() + .map(m -> "\n "+m.id().id() + + "/" + clusterManager.getBrokerUtil().getNodeStatus(m) + + "/" + m.properties().getProperty("address", "---") + + "/" + (m.isActive()?"active":"inactive") + + (!m.isReachable() ? "/unreachable" : "")) + .collect(Collectors.toList())); + + // Sleep for 5 seconds + try { Thread.sleep(delay); } catch (Exception e) {} + } + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/MemberScoreFunction.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/MemberScoreFunction.java new file mode 100644 index 0000000..6bcba33 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/MemberScoreFunction.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.cluster.Member; +import lombok.Builder; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.mariuszgromada.math.mxparser.Expression; +import org.mariuszgromada.math.mxparser.parsertokens.Token; + +import java.util.List; +import java.util.Properties; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Data +@Builder +public class MemberScoreFunction implements Function { + private final String formula; + private final double defaultScore; + private final Properties argumentDefaults; + private boolean throwExceptions; + + public MemberScoreFunction(String formula) { + this(formula, -1, new Properties(), false); + } + + public MemberScoreFunction(String formula, double defaultScore) { + this(formula, defaultScore, new Properties(), false); + } + + public MemberScoreFunction(String formula, Properties defaults) { + this(formula, -1, defaults, false); + } + + public MemberScoreFunction(String formula, double defaultScore, Properties defaults, boolean throwExceptions) { + Expression e = new Expression(formula); + //e.setVerboseMode(); + if (!e.checkLexSyntax()) + throw new IllegalArgumentException("Lexical syntax error in expression: " + e.getErrorMessage()); + this.formula = formula; + this.defaultScore = defaultScore; + this.argumentDefaults = defaults; + this.throwExceptions = throwExceptions; + } + + @Override + public Double apply(Member member) { + return evaluateExpression(formula, member.properties()); + } + + protected List getExpressionArguments(Expression e) { + // Get argument names + boolean lexSyntax = e.checkLexSyntax(); + boolean genSyntax = e.checkSyntax(); + + List initTokens = e.getCopyOfInitialTokens(); + List argNames = initTokens.stream() + .filter(t -> t.tokenTypeId == Token.NOT_MATCHED) + .filter(t -> "argument".equals(t.looksLike)) + .map(t -> t.tokenStr) + .collect(Collectors.toList()); + + return argNames; + } + + public double evaluateExpression(String formula, Properties args) { + try { + if (StringUtils.isBlank(formula)) { + throw new IllegalArgumentException("Formula is empty or null"); + } + + // Create MathParser expression + Expression e = new Expression(formula); + //e.setVerboseMode(); + + // Get argument names + List argNames = getExpressionArguments(e); + + // Define expression arguments with user provided values + //e.removeAllArguments(); + for (String argName : argNames) { + try { + String argStr = args.getProperty(argName, null); + if (StringUtils.isBlank(argStr)) + argStr = argumentDefaults.getProperty(argName, null); + if (StringUtils.isBlank(argStr)) + throw new IllegalArgumentException("Missing scoring expression argument: " + argName); + double argValue = Double.parseDouble(argStr); + e.defineArgument(argName, argValue); + } catch (Exception ex) { + throw ex; + } + } + if (!e.checkSyntax()) + throw new IllegalArgumentException("Syntax error in expression: " + e.getErrorMessage()); + + // Calculate result + return e.calculate(); + } catch (Exception ex) { + if (throwExceptions) + throw ex; + return defaultScore; + } + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/MemberWithScore.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/MemberWithScore.java new file mode 100644 index 0000000..0ed4a23 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/MemberWithScore.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.cluster.Member; +import lombok.Data; + +@Data +public class MemberWithScore implements Comparable { + public final static MemberWithScore NULL_MEMBER = new MemberWithScore(null, 0); + + private final Member member; + private final double score; + + private MemberWithScore(Member m, double s) { + member = m; + score = s; + } + + public MemberWithScore(Member m, MemberScoreFunction scoreFunction) { + member = m; + score = scoreFunction.apply(m); + } + + @Override + public int compareTo(MemberWithScore o) { + double score1 = this.getScore(); + double score2 = o.getScore(); + int result = (int) Math.signum(score1 - score2); + if (result == 0) { + String uuid1 = this.getMember().properties().getProperty("uuid", "0"); + String uuid2 = o.getMember().properties().getProperty("uuid", "0"); + result = uuid1.compareTo(uuid2); + } + return result; + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/TestCallback.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/TestCallback.java new file mode 100644 index 0000000..ad1cd83 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/cluster/TestCallback.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.cluster; + +import io.atomix.cluster.ClusterMembershipEvent; +import io.atomix.cluster.Member; +import io.atomix.utils.net.Address; + +public class TestCallback extends AbstractLogBase implements BrokerUtil.NodeCallback { + private String address; + private String state = "L1"; + + public TestCallback(Address localAddress) { + address = localAddress.toString(); + } + + public void joinedCluster() { } + public void leftCluster() { } + + public void initialize() { + if ("L2".equals(state)) { + log_warn("__TestNode at {}: Already initialized: {}", address, state); + return; + } + state = "initializing L2"; + out_print("__TestNode at {}: Initializing", address); + for (int i = 0; i < (int) (Math.random() * 5 + 5); i++) { + out_print("."); + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + } + } + out_println(); + if ("initializing L2".equals(state)) { + state = "L2"; + log_info("__TestNode at {}: Node is now a Broker: {}", address, state); + } + } + + public void stepDown() { + if ("L1".equals(state)) { + log_warn("__TestNode at {}: Already a non-broker node: {}", address, state); + return; + } + state = "clearing L2"; + out_print("__TestNode at {}: Stepping down", address); + for (int i = 0; i < (int) (Math.random() * 4 + 2); i++) { + out_print("."); + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + } + } + out_println(); + if ("clearing L2".equals(state)) { + state = "L1"; + log_info("__TestNode at {}: Node is now a non-broker node: {}", address, state); + } + } + + public void statusChanged(BrokerUtil.NODE_STATUS oldStatus, BrokerUtil.NODE_STATUS newStatus) { + log_info("__TestNode at {}: Status changed: {} --> {}", address, oldStatus, newStatus); + } + + public void clusterChanged(ClusterMembershipEvent event) { + log_info("__TestNode at {}: Cluster changed: {}: {}", address, event.type(), event.subject().id().id()); + } + + public String getConfiguration(Member local) { + return String.format("ssl://%s:61617", local.address().host()); + } + + public void setConfiguration(String newConfig) { + log_info("__TestNode at {}: New configuration: {}", address, newConfig); + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/ClientCollectorContext.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/ClientCollectorContext.java new file mode 100644 index 0000000..5bde976 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/ClientCollectorContext.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.collector; + +import gr.iccs.imu.ems.baguette.client.BaguetteClientProperties; +import gr.iccs.imu.ems.baguette.client.CommandExecutor; +import gr.iccs.imu.ems.baguette.client.Sshc; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.common.client.SshClient; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.util.ClientConfiguration; +import gr.iccs.imu.ems.util.GroupingConfiguration; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ClientCollectorContext implements CollectorContext { + private final CommandExecutor commandExecutor; + + public Map getGroupings() { + return commandExecutor.getGroupings(); + } + + @Override + public List getNodeConfigurations() { + return Collections.singletonList(commandExecutor.getClientConfiguration()); + } + + @Override + public Set getNodesWithoutClient() { + return commandExecutor.getClientConfiguration()!=null + ? commandExecutor.getClientConfiguration().getNodesWithoutClient() : null; + } + + @Override + public boolean isAggregator() { + return commandExecutor.isAggregator(); + } + + @Override + public PUBLISH_RESULT sendEvent(String connectionString, String destinationName, EventMap event, boolean createDestination) { + return commandExecutor.sendEvent(connectionString, destinationName, event, createDestination); + } + + @Override + public SshClient getSshClient() { + return new Sshc(); + } + + @Override + public BaguetteClientProperties getSshClientProperties() { + return new BaguetteClientProperties(); + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/netdata/NetdataCollector.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/netdata/NetdataCollector.java new file mode 100644 index 0000000..e41191a --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/netdata/NetdataCollector.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.collector.netdata; + +import gr.iccs.imu.ems.baguette.client.Collector; +import gr.iccs.imu.ems.baguette.client.collector.ClientCollectorContext; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.common.collector.netdata.NetdataCollectorProperties; +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.GROUPING; +import gr.iccs.imu.ems.util.GroupingConfiguration; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Collects measurements from Netdata http server + */ +@Slf4j +@Component +public class NetdataCollector extends gr.iccs.imu.ems.common.collector.netdata.NetdataCollector implements Collector { + public NetdataCollector(@NonNull NetdataCollectorProperties properties, + @NonNull CollectorContext collectorContext, + @NonNull TaskScheduler taskScheduler, + @NonNull EventBus eventBus) + { + super("NetdataCollector", properties, collectorContext, taskScheduler, eventBus); + if (!(collectorContext instanceof ClientCollectorContext)) + throw new IllegalArgumentException("Invalid CollectorContext provided. Expected: ClientCollectorContext, but got "+collectorContext.getClass().getName()); + } + + public synchronized void activeGroupingChanged(String oldGrouping, String newGrouping) { + HashSet topics = new HashSet<>(); + for (String g : GROUPING.getNames()) { + GroupingConfiguration grp = ((ClientCollectorContext)collectorContext).getGroupings().get(g); + if (grp!=null) + topics.addAll(grp.getEventTypeNames()); + } + log.warn("Collectors::Netdata: activeGroupingChanged: New Allowed Topics for active grouping: {} -- {}", newGrouping, topics); + List tmpList = new ArrayList<>(topics); + Map tmpMap = null; + if (properties.getAllowedTopics()!=null) { + tmpMap = properties.getAllowedTopics().stream() + .map(s -> s.split(":", 2)) + .collect(Collectors.toMap(a -> a[0], a -> a.length>1 ? a[1]: "")); + } + log.warn("Collectors::Netdata: activeGroupingChanged: New Allowed Topics -- Topics Map: {} -- {}", tmpList, tmpMap); + synchronized (this) { + this.allowedTopics = tmpList; + this.topicMap = tmpMap; + } + } + +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/prometheus/PrometheusCollector.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/prometheus/PrometheusCollector.java new file mode 100644 index 0000000..d0cc573 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/collector/prometheus/PrometheusCollector.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.collector.prometheus; + +import gr.iccs.imu.ems.baguette.client.Collector; +import gr.iccs.imu.ems.baguette.client.collector.ClientCollectorContext; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.common.collector.prometheus.PrometheusCollectorProperties; +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.GROUPING; +import gr.iccs.imu.ems.util.GroupingConfiguration; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Collects measurements from Prometheus exporter + */ +@Slf4j +@Component +public class PrometheusCollector extends gr.iccs.imu.ems.common.collector.prometheus.PrometheusCollector implements Collector { + public PrometheusCollector(@NonNull PrometheusCollectorProperties properties, + @NonNull CollectorContext collectorContext, + @NonNull TaskScheduler taskScheduler, + @NonNull EventBus eventBus) + { + super("PrometheusCollector", properties, collectorContext, taskScheduler, eventBus); + if (!(collectorContext instanceof ClientCollectorContext)) + throw new IllegalArgumentException("Invalid CollectorContext provided. Expected: ClientCollectorContext, but got "+collectorContext.getClass().getName()); + } + + public synchronized void activeGroupingChanged(String oldGrouping, String newGrouping) { + HashSet topics = new HashSet<>(); + for (String g : GROUPING.getNames()) { + GroupingConfiguration grp = ((ClientCollectorContext)collectorContext).getGroupings().get(g); + if (grp!=null) + topics.addAll(grp.getEventTypeNames()); + } + log.warn("Collectors::Prometheus: activeGroupingChanged: New Allowed Topics for active grouping: {} -- {}", newGrouping, topics); + List tmpList = new ArrayList<>(topics); + Map tmpMap = null; + if (properties.getAllowedTopics()!=null) { + tmpMap = properties.getAllowedTopics().stream() + .map(s -> s.split(":", 2)) + .collect(Collectors.toMap(a -> a[0], a -> a.length>1 ? a[1]: "")); + } + log.warn("Collectors::Prometheus: activeGroupingChanged: New Allowed Topics -- Topics Map: {} -- {}", tmpList, tmpMap); + synchronized (this) { + this.allowedTopics = tmpList; + this.topicMap = tmpMap; + } + } + +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/plugin/recovery/NodeInfoHelper.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/plugin/recovery/NodeInfoHelper.java new file mode 100644 index 0000000..1dd0d86 --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/plugin/recovery/NodeInfoHelper.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.plugin.recovery; + +import com.google.gson.Gson; +import gr.iccs.imu.ems.baguette.client.CommandExecutor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Node Info helper -- Retrieves node info from EMS server and caches them + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class NodeInfoHelper { + private final CommandExecutor commandExecutor; + private final HashMap nodeInfoCache = new HashMap<>(); + private final Gson gson = new Gson(); + + @SneakyThrows + public Map getNodeInfo(String nodeId, @NonNull String nodeAddress) { + log.debug("NodeInfoHelper: getNodeInfo(): BEGIN: node-id={}, node-address={}", nodeId, nodeAddress); + + // Get cached node info + Map nodeInfo = nodeInfoCache.get(nodeAddress); + + if (nodeInfo==null) { + // Get node info from EMS server + try { + log.debug("NodeInfoHelper: getNodeInfo(): Querying EMS server for Node Info: id={}, address={}", nodeId, nodeAddress); + commandExecutor.executeCommand("SEND SERVER-GET-NODE-SSH-CREDENTIALS " + nodeAddress); + String response = commandExecutor.getLastInputLine(); + log.debug("NodeInfoHelper: getNodeInfo(): Node Info from EMS server: id={}, address={}\n{}", nodeId, nodeAddress, response); + if (StringUtils.isNotBlank(response)) { + nodeInfo = gson.fromJson(response, Map.class); + } + nodeInfoCache.put(nodeAddress, nodeInfo); + } catch (Exception ex) { + log.error("NodeInfoHelper: getNodeInfo(): Exception while querying for node info: node-id={}, node-address={}\n", nodeId, nodeAddress, ex); + throw ex; + } + } + //log.debug("NodeInfoHelper: getNodeInfo(): Node info: {}", nodeInfo); + return nodeInfo; + } + + public void remove(String nodeId, @NonNull String nodeAddress) { + log.debug("NodeInfoHelper: remove(): node-id={}, node-address={}", nodeId, nodeAddress); + Map nodeInfo = nodeInfoCache.remove(nodeAddress); + log.trace("NodeInfoHelper: remove(): Removed: node-id={}, node-address={}", nodeId, nodeAddress); + } +} diff --git a/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/plugin/recovery/SelfHealingPlugin.java b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/plugin/recovery/SelfHealingPlugin.java new file mode 100644 index 0000000..39854ca --- /dev/null +++ b/ems-core/baguette-client/src/main/java/gr/iccs/imu/ems/baguette/client/plugin/recovery/SelfHealingPlugin.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.client.plugin.recovery; + +import gr.iccs.imu.ems.baguette.client.BaguetteClientProperties; +import gr.iccs.imu.ems.baguette.client.CommandExecutor; +import gr.iccs.imu.ems.baguette.client.collector.netdata.NetdataCollector; +import gr.iccs.imu.ems.common.recovery.*; +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.PasswordUtil; +import gr.iccs.imu.ems.util.Plugin; +import gr.iccs.imu.ems.util.StrUtil; +import io.atomix.cluster.ClusterMembershipEvent; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DurationFormatUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Client-side Self-Healing plugin + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SelfHealingPlugin implements Plugin, InitializingBean, EventBus.EventConsumer { + private final ApplicationContext applicationContext; + private final BaguetteClientProperties properties; + private final SelfHealingProperties selfHealingProperties; + private final CommandExecutor commandExecutor; + private final EventBus eventBus; + private final PasswordUtil passwordUtil; + private final NodeInfoHelper nodeInfoHelper; + private final RecoveryContext recoveryContext; + + private boolean started; + + private final HashMap> waitingTasks = new HashMap<>(); + private final TaskScheduler taskScheduler; + + @Override + public void afterPropertiesSet() { + log.debug("SelfHealingPlugin: properties: {}", properties); + log.debug("SelfHealingPlugin: selfHealingProperties: {}", selfHealingProperties); + + // Initialize recovery context + recoveryContext.initialize(properties); + log.warn("SelfHealingPlugin: Recovery context: {}", recoveryContext); + } + + public synchronized void start() { + // check if already running + if (started) { + log.warn("SelfHealingPlugin: Already started"); + return; + } + + eventBus.subscribe(CommandExecutor.EVENT_CLUSTER_NODE_ADDED, this); + eventBus.subscribe(CommandExecutor.EVENT_CLUSTER_NODE_REMOVED, this); + eventBus.subscribe(NetdataCollector.NETDATA_NODE_OK, this); + eventBus.subscribe(NetdataCollector.NETDATA_NODE_FAILED, this); + log.info("SelfHealingPlugin: Started"); + } + + public synchronized void stop() { + if (!started) { + log.warn("SelfHealingPlugin: Not started"); + return; + } + + eventBus.unsubscribe(CommandExecutor.EVENT_CLUSTER_NODE_ADDED, this); + eventBus.unsubscribe(CommandExecutor.EVENT_CLUSTER_NODE_REMOVED, this); + eventBus.unsubscribe(NetdataCollector.NETDATA_NODE_OK, this); + eventBus.unsubscribe(NetdataCollector.NETDATA_NODE_FAILED, this); + + // Cancel all waiting recovery tasks + waitingTasks.forEach((nodeKey,future) -> { + future.cancel(true); + }); + waitingTasks.clear(); + log.info("SelfHealingPlugin: Stopped"); + } + + @Override + public void onMessage(String topic, Object message, Object sender) { + log.debug("SelfHealingPlugin: onMessage(): BEGIN: topic={}, message={}, sender={}", topic, message, sender); + if (!selfHealingProperties.isEnabled()) return; + + // Self-Healing for EMS clients + if (CommandExecutor.EVENT_CLUSTER_NODE_REMOVED.equals(topic)) { + log.debug("SelfHealingPlugin: onMessage(): CLUSTER NODE REMOVED: message={}", message); + processClusterNodeRemovedEvent(message); + } else + if (CommandExecutor.EVENT_CLUSTER_NODE_ADDED.equals(topic)) { + log.debug("SelfHealingPlugin: onMessage(): CLUSTER NODE ADDED: message={}", message); + processClusterNodeAddedEvent(message); + } else + + // Self-healing for Netdata agents + if (NetdataCollector.NETDATA_NODE_FAILED.equals(topic)) { + log.debug("SelfHealingPlugin: onMessage(): NETDATA NODE PAUSED: message={}", message); + processNetdataNodeFailedEvent(message); + } else + if (NetdataCollector.NETDATA_NODE_OK.equals(topic)) { + log.debug("SelfHealingPlugin: onMessage(): NETDATA NODE RESUMED: message={}", message); + processNetdataNodeOkEvent(message); + } else + + // Unsupported message + { + log.debug("SelfHealingPlugin: onMessage(): Unsupported message: topic={}, message={}, sender={}", + topic, message, sender); + } + } + + // ------------------------------------------------------------------------ + + private void processClusterNodeRemovedEvent(Object message) { + log.debug("SelfHealingPlugin: processClusterNodeRemovedEvent(): BEGIN: message={}", message); + if (message instanceof ClusterMembershipEvent) { + // Get removed node id and address + ClusterMembershipEvent event = (ClusterMembershipEvent)message; + String nodeId = event.subject().id().id(); + String nodeAddress = event.subject().address().host(); + log.debug("SelfHealingPlugin: processClusterNodeRemovedEvent(): node-id={}, node-address={}", nodeId, nodeAddress); + if (StringUtils.isBlank(nodeAddress)) { + log.warn("SelfHealingPlugin: processClusterNodeRemovedEvent(): Node address is missing. Cannot recover node. Initial message: {}", event); + return; + } + + createRecoveryTask(nodeId, nodeAddress, recoveryContext, EmsClientRecoveryTask.class); + } else { + log.warn("SelfHealingPlugin: processClusterNodeRemovedEvent(): Message is not a {} object. Will ignore it.", ClusterMembershipEvent.class.getSimpleName()); + } + } + + private void processClusterNodeAddedEvent(Object message) { + log.debug("SelfHealingPlugin: processClusterNodeAddedEvent(): BEGIN: message={}", message); + if (message instanceof ClusterMembershipEvent) { + // Get added node id and address + ClusterMembershipEvent event = (ClusterMembershipEvent)message; + String nodeId = event.subject().id().id(); + String nodeAddress = event.subject().address().host(); + log.debug("SelfHealingPlugin: processClusterNodeAddedEvent(): node-id={}, node-address={}", nodeId, nodeAddress); + if (StringUtils.isBlank(nodeAddress)) { + log.warn("SelfHealingPlugin: processClusterNodeAddedEvent(): Node address is missing. Initial message: {}", event); + return; + } + + // Cancel any waiting recovery task + cancelRecoveryTask(nodeId, nodeAddress, EmsClientRecoveryTask.class, false); + } else { + log.warn("SelfHealingPlugin: processClusterNodeAddedEvent(): Message is not a {} object. Will ignore it.", ClusterMembershipEvent.class.getSimpleName()); + } + } + + // ------------------------------------------------------------------------ + + private void processNetdataNodeFailedEvent(Object message) { + log.debug("SelfHealingPlugin: processNetdataNodeFailedEvent(): BEGIN: message={}", message); + if (!(message instanceof Map)) { + log.warn("SelfHealingPlugin: processNetdataNodeFailedEvent(): Message is not a {} object. Will ignore it.", Map.class.getSimpleName()); + return; + } + + // Get paused node address + Object addressValue = StrUtil.castToMapStringObject(message).getOrDefault("address", null); + log.debug("SelfHealingPlugin: processNetdataNodeFailedEvent(): node-address={}", addressValue); + if (addressValue==null) { + log.warn("SelfHealingPlugin: processNetdataNodeFailedEvent(): Node address is missing. Cannot recover node. Initial message: {}", message); + return; + } + String nodeAddress = addressValue.toString(); + + if (isLocalAddress(nodeAddress)) { + // We are responsible for recovering our local Netdata agent + createRecoveryTask(null, "", recoveryContext, NetdataAgentLocalRecoveryTask.class); + } else { + // Aggregator is responsible for recovering remote Netdata agents + createRecoveryTask(null, nodeAddress, recoveryContext, NetdataAgentRecoveryTask.class); + } + } + + @SneakyThrows + private boolean isLocalAddress(String address) { + if (address.isEmpty()) return true; + if ("127.0.0.1".equals(address)) return true; + if ("::1".equals(address)) return true; + if ("0:0:0:0:0:0:0:1".equals(address)) return true; + InetAddress ia = InetAddress.getByName(address); + if (ia.isAnyLocalAddress() || ia.isLoopbackAddress()) return true; + try { + return NetworkInterface.getByInetAddress(ia) != null; + } catch (SocketException se) { + return false; + } + } + + private void processNetdataNodeOkEvent(Object message) { + log.debug("SelfHealingPlugin: processNetdataNodeOkEvent(): BEGIN: message={}", message); + if (!(message instanceof Map)) { + log.warn("SelfHealingPlugin: processNetdataNodeOkEvent(): Message is not a {} object. Will ignore it.", Map.class.getSimpleName()); + return; + } + + // Get resumed node address + String nodeAddress = StrUtil.castToMapStringObject(message).getOrDefault("address", "").toString(); + log.debug("SelfHealingPlugin: processNetdataNodeOkEvent(): node-address={}", nodeAddress); + /*if (StringUtils.isBlank(nodeAddress)) { + log.warn("SelfHealingPlugin: processNetdataNodeOkEvent(): Node address is missing. Initial message: {}", message); + return; + }*/ + + // Cancel any waiting recovery task + @NonNull Class recoverTaskClass = + StringUtils.isNotBlank(nodeAddress) + ? NetdataAgentRecoveryTask.class + : NetdataAgentLocalRecoveryTask.class; + cancelRecoveryTask(null, nodeAddress, recoverTaskClass, false); + } + + // ------------------------------------------------------------------------ + + private void createRecoveryTask(String nodeId, @NonNull String nodeAddress, RecoveryContext recoveryContext, @NonNull Class recoveryTaskClass) { + // Check if a recovery task has already been scheduled + NodeKey nodeKey = new NodeKey(nodeAddress, recoveryTaskClass); + synchronized (waitingTasks) { + if (waitingTasks.containsKey(nodeKey)) { + log.warn("SelfHealingPlugin: createRecoveryTask(): Recovery has already been scheduled for Node: id={}, address={}", nodeId, nodeAddress); + return; + } + waitingTasks.put(nodeKey, null); + } + + // Get node info and credentials from EMS server + Map nodeInfo = null; + if (StringUtils.isNotBlank(nodeAddress)) { + nodeInfo = nodeInfoHelper.getNodeInfo(nodeId, nodeAddress); + if (nodeInfo == null || nodeInfo.isEmpty()) { + log.warn("SelfHealingPlugin: createRecoveryTask(): Node info is null or empty. Cannot recover node."); + return; + } + log.trace("SelfHealingPlugin: createRecoveryTask(): Node info retrieved for node: id={}, address={}", nodeId, nodeAddress); + } else { + log.debug("SelfHealingPlugin: createRecoveryTask(): Node address is blank. Node info will not be retrieved: id={}, address={}", nodeId, nodeAddress); + } + + // Schedule node recovery task + final RecoveryTask recoveryTask = applicationContext.getBean(recoveryTaskClass); + if (nodeInfo!=null && !nodeInfo.isEmpty()) + recoveryTask.setNodeInfo(nodeInfo); + final AtomicInteger retries = new AtomicInteger(0); + Instant firstAttempt; + Duration retryDelay; + ScheduledFuture future = taskScheduler.scheduleWithFixedDelay( + () -> { + try { + log.info("SelfHealingPlugin: Retry #{}: Recovering node: id={}, address={}", retries.get(), nodeId, nodeAddress); + recoveryTask.runNodeRecovery(recoveryContext); + //NOTE: 'recoveryTask.runNodeRecovery()' must send SELF_HEALING_RECOVERY_COMPLETED or _FAILED event + } catch (Exception e) { + log.error("SelfHealingPlugin: EXCEPTION while recovering node: node-address={} -- Exception: ", nodeAddress, e); + eventBus.send(RecoveryConstant.SELF_HEALING_RECOVERY_FAILED, nodeAddress); + } + if (retries.getAndIncrement() >= selfHealingProperties.getRecovery().getMaxRetries()) { + log.warn("SelfHealingPlugin: Max retries reached. No more recovery retries for node: id={}, address={}", nodeId, nodeAddress); + cancelRecoveryTask(nodeId, nodeAddress, recoveryTaskClass, true); + eventBus.send(RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP, nodeAddress); + + // Notify EMS server about giving up recovery due to permanent failure + commandExecutor.notifyEmsServer("RECOVERY GIVE_UP "+nodeId+" @ "+nodeAddress); + } + }, + firstAttempt = Instant.now().plusMillis(selfHealingProperties.getRecovery().getDelay()), + retryDelay = Duration.ofMillis(selfHealingProperties.getRecovery().getRetryDelay()) + ); + waitingTasks.put(nodeKey, future); + log.info("SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id={}, address={}, first-attempt-at={}, retry-delay={}", + nodeId, nodeAddress, firstAttempt, DurationFormatUtils.formatDurationHMS(retryDelay.toMillis())); + } + + private void cancelRecoveryTask(String nodeId, @NonNull String nodeAddress, @NonNull Class recoveryTaskClass, boolean retainNodeKey) { + NodeKey nodeKey = new NodeKey(nodeAddress, recoveryTaskClass); + synchronized (waitingTasks) { + ScheduledFuture future = retainNodeKey ? waitingTasks.put(nodeKey, null) : waitingTasks.remove(nodeKey); + if (future != null) { + future.cancel(true); + nodeInfoHelper.remove(nodeId, nodeAddress); + log.info("SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id={}, address={}", nodeId, nodeAddress); + } else + log.debug("SelfHealingPlugin: cancelRecoveryTask(): No recovery task is scheduled for Node: id={}, address={}", nodeId, nodeAddress); + } + } + + @Data + @AllArgsConstructor + protected static class NodeKey { + private String address; + @NonNull private Class recoveryTaskClass; + } +} diff --git a/ems-core/baguette-client/src/main/resources/META-INF/spring.factories b/ems-core/baguette-client/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d78b4f3 --- /dev/null +++ b/ems-core/baguette-client/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=gr.iccs.imu.ems.util.NetUtilPostProcessor \ No newline at end of file diff --git a/ems-core/baguette-client/src/main/resources/banner-1.txt b/ems-core/baguette-client/src/main/resources/banner-1.txt new file mode 100644 index 0000000..b089cf8 --- /dev/null +++ b/ems-core/baguette-client/src/main/resources/banner-1.txt @@ -0,0 +1,6 @@ + ____ __ __ _________ __ + / __ )____ _____ ___ _____ / /_/ /____ / ____/ (_)__ ____ / /_ + / __ / __ `/ __ `/ / / / _ \/ __/ __/ _ \ / / / / / _ \/ __ \/ __/ + / /_/ / /_/ / /_/ / /_/ / __/ /_/ /_/ __/ / /___/ / / __/ / / / /_ +/_____/\__,_/\__, /\__,_/\___/\__/\__/\___/ \____/_/_/\___/_/ /_/\__/ + /____/ diff --git a/ems-core/baguette-client/src/main/resources/banner.txt b/ems-core/baguette-client/src/main/resources/banner.txt new file mode 100644 index 0000000..4937e13 --- /dev/null +++ b/ems-core/baguette-client/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + ____ _ _ _____ _ _ _ + | _ \ | | | | / ____| (_) | | + | |_) | __ _ __ _ _ _ ___| |_| |_ ___ | | | |_ ___ _ __ | |_ + | _ < / _` |/ _` | | | |/ _ \ __| __/ _ \ | | | | |/ _ \ '_ \| __| + | |_) | (_| | (_| | |_| | __/ |_| || __/ | |____| | | __/ | | | |_ + |____/ \__,_|\__, |\__,_|\___|\__|\__\___| \_____|_|_|\___|_| |_|\__| + __/ | + |___/ \ No newline at end of file diff --git a/ems-core/baguette-server/pom.xml b/ems-core/baguette-server/pom.xml new file mode 100644 index 0000000..bd67ebb --- /dev/null +++ b/ems-core/baguette-server/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + baguette-server + EMS - Baguette Server + + + + gr.iccs.imu.ems + broker-cep + ${project.version} + compile + + + gr.iccs.imu.ems + translator + ${project.version} + compile + + + * + * + + + + + gr.iccs.imu.ems + common + ${project.version} + + + + + org.apache.sshd + apache-sshd + ${apache-sshd.version} + pom + + + org.slf4j + slf4j-jdk14 + + + org.bouncycastle + * + + + org.springframework + * + + + + + org.apache.sshd + sshd-scp + ${apache-sshd.version} + + + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter + + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.apache.commons + commons-text + + + + + org.glassfish.jersey.core + jersey-common + 3.1.3 + + + + diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/BaguetteServer.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/BaguetteServer.java new file mode 100644 index 0000000..b3227df --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/BaguetteServer.java @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server; + +import gr.iccs.imu.ems.baguette.server.properties.BaguetteServerProperties; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.common.recovery.RecoveryConstant; +import gr.iccs.imu.ems.common.selfhealing.SelfHealingManager; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.*; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.slf4j.event.Level; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Baguette Server + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class BaguetteServer implements InitializingBean, EventBus.EventConsumer { + private final BaguetteServerProperties config; + private final PasswordUtil passwordUtil; + private final NodeRegistry nodeRegistry; + + private final EventBus eventBus; + @Getter + private final SelfHealingManager selfHealingManager; + private final TaskScheduler taskScheduler; + + private Sshd server; + + private Map> groupingTopicsMap; + private Map>> groupingRulesMap; + private Map>> topicConnections; + private Map constants; + private Set functionDefinitions; + private String upperwareGrouping; + private String upperwareBrokerUrl; + private BrokerCepService brokerCepService; + + @Override + public void afterPropertiesSet() { + // Generate a new, random username/password pair and add it to provided credentials + generateUsernamePassword(); + } + + private void generateUsernamePassword() { + String genUsername = "user-"+UUID.randomUUID(); + String genPassword = RandomStringUtils.randomAlphanumeric(32, 64); + CredentialsMap credentials = config.getCredentials(); + credentials.put(genUsername, genPassword, true); + log.info("BaguetteServer: Generated new username/password: username={}, password={}", + genUsername, credentials.getPasswordEncoder()!=null + ? credentials.getPasswordEncoder().encode(genPassword) + : passwordUtil.encodePassword(genPassword)); + } + + // Configuration getter methods + public Set getGroupingNames() { + return getGroupingNames(true); + } + + public Set getGroupingNames(boolean removeUpperware) { + Set groupings = new HashSet<>(); + groupings.addAll(groupingTopicsMap.keySet()); + groupings.addAll(groupingRulesMap.keySet()); + groupings.addAll(topicConnections.keySet()); + // remove upperware grouping (i.e. GLOBAL) + if (removeUpperware) groupings.remove(upperwareGrouping); + return groupings; + } + + private List getGroupingsSorted(boolean removeUpperware, boolean ascending) { + List list = getGroupingNames(removeUpperware).stream() + .map(GROUPING::valueOf) + .sorted() + .collect(Collectors.toList()); + if (ascending) Collections.reverse(list); + return list; + } + + private List getGroupingNamesSorted(boolean removeUpperware, boolean ascending) { + return getGroupingsSorted(removeUpperware, ascending).stream() + .map(GROUPING::name) + .collect(Collectors.toList()); + } + + private String getLowestLevelGroupingName() { + List list = getGroupingNamesSorted(false, true); + return !list.isEmpty() ? list.get(0) : null; + } + + public BaguetteServerProperties getConfiguration() { + return config; + } + + public Set getTopicsForGrouping(String grouping) { + return groupingTopicsMap.get(grouping); + } + + public Map> getRulesForGrouping(String grouping) { + return groupingRulesMap.get(grouping); + } + + public Map> getTopicConnectionsForGrouping(String grouping) { + return topicConnections.get(grouping); + } + + public Map getConstants() { + return constants; + } + + public Set getFunctionDefinitions() { + return functionDefinitions; + } + + public String getUpperwareGrouping() { return upperwareGrouping; } + + public String getUpperwareBrokerUrl() { return upperwareBrokerUrl; } + + public String getBrokerUsername() { return brokerCepService.getBrokerUsername(); } + + public String getBrokerPassword() { return brokerCepService.getBrokerPassword(); } + + public BrokerCepService getBrokerCepService() { return brokerCepService; } + + public String getServerPubkey() { return server.getPublicKey(); } + + public String getServerPubkeyFingerprint() { return server.getPublicKeyFingerprint(); } + + public String getServerPubkeyAlgorithm() { return server.getPublicKeyAlgorithm(); } + + public String getServerPubkeyFormat() { return server.getPublicKeyFormat(); } + + public NodeRegistry getNodeRegistry() { return nodeRegistry; } + + // Server control methods + public synchronized void startServer(ServerCoordinator coordinator) throws IOException { + if (server == null) { + eventBus.subscribe(RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP, this); + + log.info("BaguetteServer.startServer(): Starting SSH server..."); + nodeRegistry.setCoordinator(coordinator); + Sshd server = new Sshd(); + server.start(config, coordinator, eventBus, nodeRegistry); + server.setNodeRegistry(getNodeRegistry()); + this.server = server; + log.info("BaguetteServer.startServer(): Starting SSH server... done"); + } else { + log.info("BaguetteServer.startServer(): SSH server is already running"); + } + } + + public synchronized void stopServer() throws IOException { + if (server != null) { + eventBus.unsubscribe(RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP, this); + + log.info("BaguetteServer.setServerConfiguration(): stopping SSH server..."); + server.stop(); + this.server = null; + nodeRegistry.setCoordinator(null); + log.info("BaguetteServer.setServerConfiguration(): stopping SSH server... done"); + } else { + log.info("BaguetteServer.stop(): No SSH server instance is running"); + } + } + + public synchronized void restartServer(ServerCoordinator coordinator) throws IOException { + stopServer(); + startServer(coordinator); + } + + public synchronized boolean isServerRunning() { + return server != null; + } + + @Override + public void onMessage(String topic, Object message, Object sender) { + log.trace ("BaguetteServer.onMessage: BEGIN: topic={}, message={}, sender={}", topic, message, sender); + + String nodeAddress = (message!=null) ? message.toString() : null; + log.trace("BaguetteServer.onMessage: nodeAddress={}", nodeAddress); + + if (RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP.equals(topic)) { + if (StringUtils.isNotBlank(nodeAddress)) { + NodeRegistryEntry node = nodeRegistry.getNodeByAddress(nodeAddress); + if (node!=null) { + node.nodeFailed(null); + log.info("BaguetteServer.onMessage: Marked Node as Failed: {}", nodeAddress); + } else { + log.warn("BaguetteServer.onMessage: Node with Address not found: {}", nodeAddress); + log.debug("BaguetteServer.onMessage: Node addresses: {}", nodeRegistry.getNodeAddresses()); + } + } + } else { + log.warn("BaguetteServer.onMessage: Event from unexpected topic received. Ignoring it: {}", topic); + } + } + + // Topology configuration methods + public synchronized void setTopologyConfiguration( + TranslationContext _TC, + Map constants, + String upperwareGrouping, + BrokerCepService brokerCepService) + throws IOException + { + log.debug("BaguetteServer.setTopologyConfiguration(): BEGIN"); + + // Set new configuration + this.groupingTopicsMap = _TC.getG2T(); + this.groupingRulesMap = _TC.getG2R(); + this.topicConnections = _TC.getTopicConnections(); + this.constants = constants; + this.functionDefinitions = _TC.getFunctionDefinitions(); + this.upperwareGrouping = upperwareGrouping; + this.upperwareBrokerUrl = brokerCepService.getBrokerCepProperties().getBrokerUrlForClients(); + this.brokerCepService = brokerCepService; + + // Print new configuration + log.debug("BaguetteServer.setTopologyConfiguration(): Grouping-to-Topics (G2T): {}", groupingTopicsMap); + log.debug("BaguetteServer.setTopologyConfiguration(): Grouping-to-Rules (G2R): {}", groupingRulesMap); + log.debug("BaguetteServer.setTopologyConfiguration(): Topic-Connections: {}", topicConnections); + log.debug("BaguetteServer.setTopologyConfiguration(): Constants: {}", constants); + log.debug("BaguetteServer.setTopologyConfiguration(): Function-Definitions: {}", functionDefinitions); + log.debug("BaguetteServer.setTopologyConfiguration(): Upperware-grouping: {}", upperwareGrouping); + log.debug("BaguetteServer.setTopologyConfiguration(): Upperware-broker-url: {}", upperwareBrokerUrl); + log.debug("BaguetteServer.setTopologyConfiguration(): Broker-credentials: username={}, password={}", + brokerCepService.getBrokerUsername(), passwordUtil.encodePassword(brokerCepService.getBrokerPassword())); + + // Stop any running instance of SSH server + stopServer(); + + // Clear node registry + nodeRegistry.clearNodes(); + + log.debug("BaguetteServer.setTopologyConfiguration(): Baguette server configuration: {}", config); + log.debug("BaguetteServer.setTopologyConfiguration(): Baguette Server credentials: {}", config.getCredentials()); + + // Initialize server coordinator + log.debug("BaguetteServer.setTopologyConfiguration(): Initializing Baguette protocol coordinator..."); + ServerCoordinator coordinator = createServerCoordinator(config, _TC, upperwareGrouping); + log.debug("BaguetteServer.setTopologyConfiguration(): Coordinator: {}", coordinator.getClass().getName()); + coordinator.initialize(_TC, upperwareGrouping, this, () -> + { + log.info("****************************************"); + log.info("**** MONITORING TOPOLOGY IS READY ****"); + log.info("****************************************"); + } + ); + + // Start a new instance of SSH server + startServer(coordinator); + + log.debug("BaguetteServer.setTopologyConfiguration(): END"); + } + + protected static ServerCoordinator createServerCoordinator(BaguetteServerProperties config, TranslationContext _TC, String upperwareGrouping) { + // Initialize coordinator class and parameters for backward compatibility + Class coordinatorClass = config.getCoordinatorClass(); + Map coordinatorParams = config.getCoordinatorParameters(); + + // Check if Coordinator Id has been specified (this overrides) + for (String id : config.getCoordinatorId()) { + if (StringUtils.isBlank(id)) + throw new IllegalArgumentException("Coordinator Id cannot be null or blank"); + + // Get coordinator class and parameters by Id + BaguetteServerProperties.CoordinatorConfig coordConfig = config.getCoordinatorConfig().get(id); + if (coordConfig == null) + throw new IllegalArgumentException("Not found coordinator configuration with id: " + id); + coordinatorClass = coordConfig.getCoordinatorClass(); + if (coordinatorClass == null) + throw new IllegalArgumentException("Not found coordinator class in configuration with id: " + id); + coordinatorParams = coordConfig.getParameters(); + + // Initialize coordinator instance + ServerCoordinator coordinator = createServerCoordinator(id, coordinatorClass, coordinatorParams, _TC, upperwareGrouping); + + if (coordinator != null) + return coordinator; + // else try the next coordinator in configuration + } + + if (coordinatorClass == null) + throw new IllegalArgumentException("Either coordinator class or coordinator id must be specified"); + + // Initialize coordinator class and parameters for backward compatibility + ServerCoordinator coordinator = createServerCoordinator(null, coordinatorClass, coordinatorParams, _TC, upperwareGrouping); + if (coordinator == null) { + log.error("No configured coordinator supports Translation Context.\nCoordinator Id's: {}\nDefault coordinator: {}\nTranslation Context:\n{}", + config.getCoordinatorId(), coordinatorClass, _TC); + throw new IllegalArgumentException("No configured coordinator supports Translation Context"); + } + return coordinator; + } + + @SneakyThrows + private static ServerCoordinator createServerCoordinator(String id, Class coordinatorClass, Map coordinatorParams, TranslationContext _TC, String upperwareGrouping) { + log.debug("createServerCoordinator: Instantiating coordinator with id: {}", id); + + // Initialize coordinator instance + ServerCoordinator coordinator = coordinatorClass.getConstructor().newInstance(); + + // Set coordinator parameters + coordinator.setProperties(coordinatorParams); + + // Check if coordinator supports this Translation Context + if (!coordinator.isSupported(_TC)) { + log.debug("createServerCoordinator: Coordinator does not support Translation Context: id={}", id); + return null; + } + + log.debug("createServerCoordinator: Coordinator supports Translation Context: id={}", id); + return coordinator; + } + + public void sendToActiveClients(String command) { + server.sendToActiveClients(command); + } + + public void sendToClient(String clientId, String command) { + server.sendToClient(clientId, command); + } + + public void sendToActiveClusters(String command) { + server.sendToActiveClusters(command); + } + + public void sendToCluster(String clusterId, String command) { + server.sendToCluster(clusterId, command); + } + + public Object readFromClient(String clientId, String command, Level logLevel) { + return server.readFromClient(clientId, command, logLevel); + } + + public List getActiveClients() { + return ClientShellCommand.getActive().stream() + .map(c -> { + NodeRegistryEntry entry = getNodeRegistryEntryFromClientShellCommand(c); + return formatClientList(c, entry); + }) + .sorted() + .collect(Collectors.toList()); + } + + public Map> getActiveClientsMap() { + return ClientShellCommand.getActive().stream() + .map(c -> { + NodeRegistryEntry entry = getNodeRegistryEntryFromClientShellCommand(c); + return prepareClientMap(c, entry); + }) + .sorted(Comparator.comparing(m -> m.get("id"))) + .collect(Collectors.toMap(m -> m.get("id"), m -> m, + (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, + LinkedHashMap::new)); + } + + private NodeRegistryEntry getNodeRegistryEntryFromClientShellCommand(ClientShellCommand c) { + NodeRegistryEntry entry = c.getNodeRegistryEntry(); + if (entry==null) + entry = getNodeRegistry().getNodeByAddress(c.getClientIpAddress()); + log.debug("getNodeRegistryEntryFromClientShellCommand: CSC ip-address: {}", c.getClientIpAddress()); + log.debug("getNodeRegistryEntryFromClientShellCommand: CSC NR entry: {}", entry!=null ? entry.getPreregistration() : null); + /*if (entry==null) { + log.warn("getNodeRegistryEntryFromClientShellCommand: WARN: ** NOT SECURE ** CSC client-id: {}", c.getClientId()); + entry = getNodeRegistry().getNodeByClientId(c.getClientId()); + log.debug("getNodeRegistryEntryFromClientShellCommand: WARN: ** NOT SECURE ** CSC NR entry: {}", entry!=null ? entry.getPreregistration() : null); + }*/ + return entry; + } + + public List getNodesWithoutClient() { + return createClientList(new HashSet<>(Collections.singletonList(NodeRegistryEntry.STATE.NOT_INSTALLED))); + } + + public Map> getNodesWithoutClientMap() { + return createClientMap(new HashSet<>(Collections.singletonList(NodeRegistryEntry.STATE.NOT_INSTALLED))); + } + + public List getIgnoredNodes() { + return createClientList(new HashSet<>(Collections.singletonList(NodeRegistryEntry.STATE.IGNORE_NODE))); + } + + public Map> getIgnoredNodesMap() { + return createClientMap(new HashSet<>(Collections.singletonList(NodeRegistryEntry.STATE.IGNORE_NODE))); + } + + public List getPassiveNodes() { + return createClientList(new HashSet<>(Arrays.asList(NodeRegistryEntry.STATE.NOT_INSTALLED, NodeRegistryEntry.STATE.IGNORE_NODE))); + } + + public Map> getPassiveNodesMap() { + return createClientMap(new HashSet<>(Arrays.asList(NodeRegistryEntry.STATE.NOT_INSTALLED, NodeRegistryEntry.STATE.IGNORE_NODE))); + } + + public List getAllNodes() { + return createClientList(new HashSet<>(Arrays.asList(NodeRegistryEntry.STATE.values()))); + } + + public Map> getAllNodesMap() { + return createClientMap(new HashSet<>(Arrays.asList(NodeRegistryEntry.STATE.values()))); + } + + private List createClientList(Set states) { + return nodeRegistry.getNodes().stream() + .filter(entry->states.contains(entry.getState())) + .map(entry -> { + log.debug("createClientList: Node ip-address: {}", entry.getIpAddress()); + log.debug("createClientList: Node preregistration info: {}", entry.getPreregistration()); + ClientShellCommand c = getClientShellCommandFromNodeRegistryEntry(entry); + return formatClientList(c, entry); + }) + .sorted() + .collect(Collectors.toList()); + } + + private Map> createClientMap(Set states) { + return nodeRegistry.getNodes().stream() + .filter(entry -> states.contains(entry.getState())) + .sorted(Comparator.comparing(NodeRegistryEntry::getClientId)) + .collect(Collectors.toMap(NodeRegistryEntry::getClientId, entry -> { + log.debug("createClientMap: Node ip-address: {}", entry.getIpAddress()); + log.debug("createClientMap: Node preregistration info: {}", entry.getPreregistration()); + ClientShellCommand c = getClientShellCommandFromNodeRegistryEntry(entry); + return prepareClientMap(c, entry); + }, (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, LinkedHashMap::new)); + } + + private ClientShellCommand getClientShellCommandFromNodeRegistryEntry(NodeRegistryEntry entry) { + return StringUtils.isNotBlank(entry.getIpAddress()) + ? ClientShellCommand.getActiveByIpAddress(entry.getIpAddress()) : null; + } + + private String formatClientList(ClientShellCommand c, NodeRegistryEntry entry) { + final StringBuilder sb = new StringBuilder(); + prepareClientMap(c, entry).forEach((k,v)->{ + if ("id".equals(k)) sb.append(v); + else if ("node-port".equals(k)) sb.append(":").append(v); + else sb.append(" ").append(v); + }); + return sb.toString(); + } + + private Map prepareClientMap(ClientShellCommand c, NodeRegistryEntry entry) { + // Get node hostname + String address = entry!=null ? entry.getIpAddress() : c.getClientIpAddress(); + String hostname = entry!=null ? entry.getHostname() : null; + if (StringUtils.isBlank(hostname)) { + if (c!=null) + hostname = c.getClientClusterNodeHostname(); + if (StringUtils.isNotBlank(hostname)) { + if (c!=null) c.setClientClusterNodeHostname(hostname); + if (entry!=null) entry.setHostname(hostname); + } + + // Resolve hostname in a separate thread to avoid blocking this method (and the Web Admin updates) + if (config.isResolveHostname() && StringUtils.isBlank(hostname)) { + taskScheduler.schedule(()->{ + try { + String _hostname = InetAddress.getByName(address).getHostName(); + if (StringUtils.isNotBlank(_hostname)) { + if (c!=null) c.setClientClusterNodeHostname(_hostname); + if (entry!=null) entry.setHostname(_hostname); + } + } catch (Exception e) { + log.warn("Failed to resolve client hostname from IP address: {}\n", address, e); + } + }, Instant.now()); + } + } + + // Prepare node info map + Map properties = new LinkedHashMap<>(); + properties.put("id", c!=null ? c.getId() : entry.getClientId()); + properties.put("ip-address", address); + properties.put("node-hostname", c!=null ? c.getClientClusterNodeHostname() : hostname); + properties.put("node-port", Integer.toString(c!=null ? c.getClientClusterNodePort() : -1)); + properties.put("node-status", c!=null ? c.getClientNodeStatus() : null); + properties.put("node-zone", (entry!=null && entry.getClusterZone()!=null) ? entry.getClusterZone().getId() : null); //c.getClientZone()!=null ? c.getClientZone().getId() : null + properties.put("grouping", c!=null ? c.getClientGrouping() : (entry.getState()==NodeRegistryEntry.STATE.NOT_INSTALLED ? getLowestLevelGroupingName() : null)); + properties.put("reference", entry!=null ? entry.getReference() : null); + properties.put("node-id", c!=null ? c.getClientProperty("node-id") : null); + properties.put("node-state", entry!=null && entry.getState()!=null ? entry.getState().toString() : null); + properties.put("errors", entry!=null && entry.getErrors()!=null + ? entry.getErrors().stream() + .filter(Objects::nonNull) + .map(Object::toString) + .collect(Collectors.joining(" | ")) + : null); + return properties; + } + + public void sendConstants(Map constants) { + server.sendConstants(constants); + } + + public NodeRegistryEntry registerClient(Map nodeInfoMap) throws UnknownHostException { + log.debug("BaguetteServer.registerClient(): node-info={}", nodeInfoMap); + + Map nodeInfo = new HashMap<>(nodeInfoMap); + + // Create client id and random UUID + String clientId = nodeInfoMap.get("CLIENT_ID")!=null && StringUtils.isNotBlank(nodeInfoMap.get("CLIENT_ID").toString()) + ? nodeInfoMap.get("CLIENT_ID").toString() + : generateClientIdFromNodeInfo(nodeInfo); + Object randomUuid = UUID.randomUUID().toString(); + nodeInfo.put("random", randomUuid); + log.debug("BaguetteServer.registerClient(): client-id={}, random-UUID={}", clientId, randomUuid); + + // Add node info into node registry + return nodeRegistry.addNode(nodeInfo, clientId); + } + + public String generateClientIdFromNodeInfo(Map nodeInfo) { + String clientId; + String formatter = getConfiguration().getClientIdFormat(); + if (StringUtils.isBlank(formatter)) { + log.debug("BaguetteServer.registerClient(): No formatter specified. A random uuid will be returned"); + clientId = UUID.randomUUID().toString(); + } else { + String escape = Optional.ofNullable(getConfiguration().getClientIdFormatEscape()).orElse("~"); + formatter = formatter.replace(escape,"$"); + log.debug("BaguetteServer.registerClient(): formatter={}", formatter); + clientId = StringSubstitutor.replace(formatter, nodeInfo); + } + return clientId; + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/ClientShellCommand.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/ClientShellCommand.java new file mode 100644 index 0000000..e873f28 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/ClientShellCommand.java @@ -0,0 +1,735 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.gson.Gson; +import gr.iccs.imu.ems.baguette.server.coordinator.cluster.IClusterZone; +import gr.iccs.imu.ems.common.recovery.RecoveryConstant; +import gr.iccs.imu.ems.util.*; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.session.SessionListener; +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.command.Command; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.session.ServerSessionAware; +import org.cryptacular.util.CertUtil; +import org.slf4j.event.Level; + +import javax.validation.constraints.NotBlank; +import java.io.*; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Slf4j +public class ClientShellCommand implements Command, Runnable, ServerSessionAware { + + private final static Object LOCK = new Object(); + private final static AtomicLong counter = new AtomicLong(0); + private final static Set activeCmdList = new HashSet<>(); + private final static Map activeCmdMap = new HashMap<>(); + private final static long INPUT_CHECK_DELAY = 100; + + public static Set getActive() { + return Collections.unmodifiableSet(activeCmdList); + } + + public static Set getActiveIds() { + return Collections.unmodifiableSet(activeCmdMap.keySet()); + } + + public static ClientShellCommand getActiveByIpAddress(@NotBlank String address) { + return activeCmdMap.get(address); + } + + public static ClientShellCommand getActiveById(@NotBlank String id) { + return activeCmdList.stream().filter(csc->csc.getId().equals(id)).findFirst().orElse(null); + } + + private InputStream in; + private PrintStream out; + private PrintStream err; + private ExitCallback callback; + private final AtomicBoolean callbackCalled = new AtomicBoolean(false); + + @Getter @Setter + private String id; + @Getter @Setter + private boolean echoOn = false; + + private String clientId; + @Getter private String clientBrokerUrl; + @Getter private String clientBrokerUsername; + @Getter private String clientBrokerPassword; + private String clientIpAddress; + private String clientHostname; + private String clientCanonicalHostname; + private int clientPort = -1; + @Getter private String clientCertificate; // Broker certificate of Client + + @Getter @Setter private int clientClusterNodePort; + @Getter @Setter private String clientClusterNodeAddress; + @Getter @Setter private String clientClusterNodeHostname; + @Getter @Setter private IClusterZone clientZone; + @Getter private String clientNodeStatus; + @Getter private String clientGrouping; + private final Properties clientProperties = new Properties(); + + private final ServerCoordinator coordinator; + private final boolean clientAddressOverrideAllowed; + @Getter + private ServerSession session; + @Getter @Setter + private boolean closeConnection = false; + + private final Map inputsMap = new HashMap<>(); + private final EventBus eventBus; + @Getter + private Exception lastException; + @JsonIgnore + private final transient NodeRegistry nodeRegistry; + @Setter + private NodeRegistryEntry nodeRegistryEntry; + + @Getter + private Map clientStatistics; + + public ClientShellCommand(ServerCoordinator coordinator, boolean allowClientOverrideItsAddress, EventBus eventBus, NodeRegistry registry) { + synchronized (LOCK) { + id = String.format("#%05d", counter.getAndIncrement()); + } + this.coordinator = coordinator; + this.clientAddressOverrideAllowed = allowClientOverrideItsAddress; + this.eventBus = eventBus; + this.nodeRegistry = registry; + } + + @JsonIgnore + public NodeRegistry getNodeRegistry() { + return nodeRegistry; + } + + public void setSession(ServerSession session) { + log.info("{}--> Got session : {}", id, session); + this.session = session; + eventBus.send("BAGUETTE_SERVER_CLIENT_SESSION_STARTED", this); + + /*try { + String clientIpAddr = ((InetSocketAddress)session.getIoSession().getRemoteAddress()).getAddress().getHostAddress(); + int clientPort = ((InetSocketAddress)session.getIoSession().getRemoteAddress()).getPort(); + log.info("{}--> Client connection : {}:{}", id, clientIpAddr, clientPort); + String username = session.getUsername(); + log.info("{}--> Client session username: {}", username); + } catch (Exception ex) {}*/ + + session.addSessionListener(new SessionListener() { + @Override + public void sessionException(Session session, Throwable t) { + log.warn("{}--> SessionListener: sessionException Throwable: ", id, t); + } + @Override + public void sessionClosed(Session session) { + log.info("{}--> SessionListener: sessionClosed", id); + } + }); + + // Initialize NodeRegistryEntry for this CSC + initNodeRegistryEntry(); + } + + private void initNodeRegistryEntry() { + String address = getClientIpAddress(); + NodeRegistryEntry entry = coordinator.getServer().getNodeRegistry().getNodeByAddress(address); + log.debug("{}--> initNodeRegistryEntry: Node registry entry for CSC: address={}, entry={}", id, address, entry); + log.trace("{}--> initNodeRegistryEntry: Current nodeRegistryEntry: {}", id, entry); + if (entry!=null) { + setNodeRegistryEntry(entry); + } else { + log.error("{}--> initNodeRegistryEntry: No node registry entry found for client: address={}", id, address); + log.error("{}--> initNodeRegistryEntry: Marked client session for immediate close: address={}", id, address); + setCloseConnection(true); + } + } + + public void setInputStream(InputStream in) { + this.in = in; + } + + public void setOutputStream(OutputStream out) { + this.out = new PrintStream(out, true); + } + + public void setErrorStream(OutputStream err) { + this.err = new PrintStream(err, true); + } + + public void setExitCallback(ExitCallback callback) { + this.callback = callback; + } + + @Override + public void start(ChannelSession channelSession, Environment environment) throws IOException { + new Thread(this).start(); + } + + @Override + public void destroy(ChannelSession channelSession) throws Exception { + } + + public void run() { + // Check if session has been marked for immediate close + if (closeConnection) { + log.warn("{}--> Exiting immediately because 'closeConnection' flag is set", id); + eventBus.send("BAGUETTE_SERVER_CLIENT_SESSION_CLOSING_IMMEDIATELY", this); + coordinator.unregister(this); + if (this.session!=null && this.session.isOpen()) { + try { + this.session.close(); + } catch (IOException e) { + log.warn("Closing session caused on exception: ", e); + } + this.session = null; + } + if (!callbackCalled.getAndSet(true)) { + callback.onExit(2); + } + log.info("{}--> Thread stopped immediately", id); + eventBus.send("BAGUETTE_SERVER_CLIENT_SESSION_CLOSED_IMMEDIATELY", this); + return; + } + + // Add this CSC in active list + synchronized (activeCmdList) { + if (activeCmdMap.containsKey(getClientIpAddress()) || activeCmdMap.containsValue(this)) + throw new IllegalArgumentException("ClientShellCommand has already been registered"); + activeCmdList.add(this); + activeCmdMap.put(getClientIpAddress(), this); + } + eventBus.send("BAGUETTE_SERVER_CLIENT_STARTING", this); + getNodeRegistryEntry().nodeRegistering(null); + + // Process client input + try { + log.info("{}==> Thread started", id); + out.printf("CLIENT (%s) : START\n", id); + + this.clientIpAddress = getClientIpAddress(); + + // Enter the main processing loop + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line; + boolean helloReceived = false; + while ((line = reader.readLine()) != null) { + line = line.trim(); + log.debug("{}--> {}", id, line); + + // Echo command (if configured) + //if (echoOn) out.printf("CLIENT (%s) : ECHO : %s\n", id, line); + if (echoOn) out.printf("ECHO %s\n", line); + //if (line.equalsIgnoreCase("exit")) break; + + if (!helloReceived && line.startsWith("-HELLO FROM CLIENT:")) { + // Process the Greeting line from client -- It must be the first line received + helloReceived = true; + getClientInfoFromGreeting(line.substring("-HELLO FROM CLIENT:".length())); + + // Register CSC to Coordinator + coordinator.register(this); + eventBus.send("BAGUETTE_SERVER_CLIENT_REGISTERED", this); + getNodeRegistryEntry().nodeRegistered(null); + + // Instruct client to start sending statistics + sendCommand("SEND-STATS START"); + } else { + // Process the subsequent lines from client -- After the Greeting line + processClientInput(line); + } + } + // Client connection closed + eventBus.send("BAGUETTE_SERVER_CLIENT_EXITING", this); + getNodeRegistryEntry().nodeExiting(null); + + log.info("{}==> Signaling client to exit", id); + out.println("EXIT"); + + } catch (Exception ex) { + log.warn("{}==> EXCEPTION : ", id, ex); + out.printf("EXCEPTION %s\n", ex); + this.lastException = ex; + eventBus.send("BAGUETTE_SERVER_CLIENT_EXCEPTION", this); + NodeRegistryEntry entry = getNodeRegistryEntry(); + if (entry.getState()==NodeRegistryEntry.STATE.REGISTERING) entry.nodeRegistrationError(ex); + else entry.nodeDisconnected(ex); + } finally { + // Remove CSC from active list + synchronized (activeCmdList) { + activeCmdList.remove(this); + activeCmdMap.remove(getClientIpAddress()); + } + log.info("{}--> Thread stops", id); + + // Unregister from Coordinator + coordinator.unregister(this); + eventBus.send("BAGUETTE_SERVER_CLIENT_UNREGISTERED", this); + + // Invoke callback if provided + if (!callbackCalled.getAndSet(true)) { + callback.onExit(0); + } + eventBus.send("BAGUETTE_SERVER_CLIENT_EXITED", this); + if (getNodeRegistryEntry().getState()==NodeRegistryEntry.STATE.EXITING) + getNodeRegistryEntry().nodeExited(null); + } + } + + private void processClientInput(String line) throws IOException, ClassNotFoundException { + if (line.startsWith("-INPUT:")) { + String input = line.substring("-INPUT:".length()); + String[] part = input.split(":",2 ); + inputsMap.put(part[0].trim(), SerializationUtil.deserializeFromString(part[1])); + } else if (StringUtils.startsWithIgnoreCase(line, "SERVER-")) { + String[] lineArgs = line.split(" ", 2); + if ("SERVER-GET-NODE-SSH-CREDENTIALS".equalsIgnoreCase(lineArgs[0].trim()) && lineArgs.length>1) { + String nodeAddress = lineArgs[1].trim(); + if (!nodeAddress.isEmpty()) { + NodeRegistryEntry entry = nodeRegistry.getNodeByAddress(nodeAddress); + if (entry!=null) { + Map preregInfo = entry.getPreregistration(); + log.debug("{}--> NODE PRE-REGISTRATION INFO: address={}\n{}", getId(), nodeAddress, preregInfo); + + if (preregInfo!=null) { + String preregInfoStr = new Gson().toJson(preregInfo); + log.trace("{}--> NODE PRE-REGISTRATION INFO STRING: STR={}\n{}", getId(), nodeAddress, preregInfoStr); + sendToClient(preregInfoStr); + } else { + log.warn("{}--> NO PRE-REGISTRATION INFO FOR NODE: {}", getId(), nodeAddress); + sendToClient("{}"); + } + } else { + log.warn("{}--> UNKNOWN NODE: {}", getId(), nodeAddress); + sendToClient("{}"); + } + } + } + } else if (line.startsWith("-NOTIFY-GROUPING-CHANGE:")) { + String newGrouping = line.substring("-NOTIFY-GROUPING-CHANGE:".length()).trim(); + log.info("{}--> Client grouping changed: {} --> {}", getId(), clientGrouping, newGrouping); + if (StringUtils.isNotBlank(newGrouping) && ! StringUtils.equals(clientGrouping, newGrouping)) + this.clientGrouping = newGrouping; + } else if (line.startsWith("-NOTIFY-STATUS-CHANGE:")) { + String newNodeStatus = line.substring("-NOTIFY-STATUS-CHANGE:".length()).trim(); + log.info("{}--> Client status changed: {} --> {}", getId(), clientNodeStatus, newNodeStatus); + if (StringUtils.isNotBlank(newNodeStatus) && ! StringUtils.equals(clientNodeStatus, newNodeStatus)) + this.clientNodeStatus = newNodeStatus; + } else if (line.startsWith("-NOTIFY-X:")) { + String message = line.substring("-NOTIFY-X:".length()).trim(); + String[] part = message.split(" ", 2); + String command = part[0].trim(); + String args = part.length>1 ? part[1] : null; + log.info("{}--> Client notification: CMD={}, ARGS={}", getId(), command, args); + + if ("DEBUG".equalsIgnoreCase(command)) { + log.debug("{}--> {}", getId(), args); + } else + if ("INFO".equalsIgnoreCase(command)) { + log.info("{}--> {}", getId(), args); + } else + if ("WARN".equalsIgnoreCase(command)) { + log.warn("{}--> {}", getId(), args); + } else + if ("ERROR".equalsIgnoreCase(command)) { + log.error("{}--> {}", getId(), args); + } else + if ("RECOVERY".equalsIgnoreCase(command)) { + args = args==null ? "" : args; + part = args.split(" ", 2); + String notificationType = part[0].trim(); + String clientData = part.length>1 ? part[1] : null; + if (StringUtils.isNotBlank(notificationType) && StringUtils.isNotBlank(clientData)) { + log.info("{}--> Client Recovery Notification: {}: {}", getId(), notificationType, clientData); + if ("GIVE_UP".equalsIgnoreCase(notificationType)) { + String[] tmp = clientData.split("@", 2); + String nodeId = tmp[0].trim(); + String nodeAddress = tmp.length>1 ? tmp[1].trim() : null; + if (StringUtils.isNotBlank(nodeAddress)) + eventBus.send(RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP, nodeAddress, "Client_" + getId()); + else + log.warn("{}--> Missing Node Address in Client Recovery Notification: {}", getId(), args); + } else + log.warn("{}--> UNKNOWN Client Recovery Notification: {}", getId(), args); + } else { + log.warn("{}--> INVALID Client Recovery Notification: {}", getId(), args); + } + } else + { + log.warn("{}--> UNKNOWN Client Notification type: {}", getId(), message); + } + + } else if (line.startsWith("-CLIENT-PROPERTY-CHANGE:")) { + String[] part = line.substring("-CLIENT-PROPERTY-CHANGE:".length()).trim().split(" ", 2); + String propertyName = part[0]; + String propertyValue = part.length > 1 ? part[1] : null; + String oldValue = clientProperties.getProperty(propertyName); + if (StringUtils.isNotBlank(propertyName)) { + log.info("{}--> Client property changed: {} = {} --> {}", getId(), propertyName, oldValue, propertyValue); + clientProperties.put(propertyName.trim(), propertyValue); + } else { + log.warn("{}--> Invalid Client property: input line: ", line); + } + } else if (line.startsWith("-STATS:")) { + String statsStr = line.substring("-STATS:".length()); + Object statsObj = SerializationUtil.deserializeFromString(statsStr); + if (statsObj instanceof Map) { + Map statsMap = StrUtil.castToMapStringObject(statsObj); + statsMap.put("_received_at_server_timestamp", System.currentTimeMillis()); + log.debug("{}--> Client STATS received: {}", getId(), statsMap); + this.clientStatistics = statsMap; + } else if (statsObj==null) { + log.debug("{}--> Client STATS object is NULL", getId()); + } else { + log.error("{}--> Unsupported Client STATS object: class={}, object={}", getId(), statsObj.getClass().getName(), statsObj); + } + } else if (line.equalsIgnoreCase("READY")) { + coordinator.clientReady(this); + } else { + coordinator.processClientInput(this, line); + } + } + + protected void getClientInfoFromGreeting(String greetingInfo) { + if (StringUtils.isBlank(greetingInfo)) return; + String[] clientInfo = greetingInfo.trim().split(" "); + + for (String s : clientInfo) { + if (StringUtils.isBlank(s)) continue; + if (s.startsWith("id=")) { + this.clientId = s.substring("id=".length()).replace("~~", " "); + log.info("{}--> Client Id: {}", id, clientId); + } else + if (s.startsWith("broker=")) { + this.clientBrokerUrl = s.substring("broker=".length()); + log.info("{}--> Broker URL: {}", id, clientBrokerUrl); + } else + if (s.startsWith("address=")) { + if (clientAddressOverrideAllowed) { + String addr = s.substring("address=".length()); + if (StringUtils.isNotBlank(addr)) { + this.clientIpAddress = addr.trim(); + log.info("{}--> Effective IP: {}", id, clientIpAddress); + } + } + } else + if (s.startsWith("port=")) { + if (clientAddressOverrideAllowed) { + try { + int port = Integer.parseInt(s.substring("port=".length())); + if (port>0 && port<65536) { + this.clientPort = port; + log.info("{}--> Effective Port: {}", id, clientPort); + } + } catch (Exception ex) { + log.warn("{}--> Invalid Port value: {}: {}", id, s.substring("port=".length()), ex.getMessage()); + } + } + } else + if (s.startsWith("username=")) { + this.clientBrokerUsername = s.substring("username=".length()); + log.info("{}--> Broker Username: {}", id, clientBrokerUsername); + } else + if (s.startsWith("password=")) { + this.clientBrokerPassword = s.substring("password=".length()); + log.info("{}--> Broker Password: {}", id, PasswordUtil.getInstance().encodePassword(clientBrokerPassword)); + } else + if (s.startsWith("cert=")) { + this.clientCertificate = s.substring("cert=".length()) + .replace("~~", " ") + .replace("##", "\r\n") + .replace("$$", "\n"); + log.info("{}--> Broker Cert.: {}", id, clientCertificate); + + // Get certificate alias from client Id or IP address + String alias = /*StringUtils.isNotBlank(clientId) + ? clientId.trim() + :*/ getClientIpAddress(); + log.info("{}--> Adding/Replacing client certificate in Truststore: alias={}", id, alias); + + if (StringUtils.isNotEmpty(clientCertificate)) { + // Add certificate to truststore + try { + X509Certificate cert = (X509Certificate) coordinator + .getServer() + .getBrokerCepService() + .addOrReplaceCertificateInTruststore(alias, clientCertificate); + log.info("{}--> Added/Replaced client certificate in Truststore: alias={}, CN={}, certificate-names={}", + id, alias, cert.getSubjectX500Principal().getName(), CertUtil.subjectNames(cert)); + } catch (Exception e) { + log.warn("{}--> EXCEPTION while adding/replacing certificate in Trust store: alias={}, exception: ", + clientId, alias, e); + } + } else { + log.info("{}--> Client PEM certificate is empty. Leaving truststore unchanged", id); + } + } else { + log.warn("{}--> Unknown HELLO argument will be ignored: {}", id, s); + } + } + + if (StringUtils.isBlank(this.clientId) || "null".equalsIgnoreCase(this.clientId)) + this.clientId = getClientId(); + if (StringUtils.isBlank(this.clientIpAddress) || "null".equalsIgnoreCase(this.clientIpAddress)) + this.clientIpAddress = getClientIpAddress(); + if (this.clientPort<=0 || this.clientPort>65535) + this.clientPort = getClientPort(); + } + + public String getClientId() { + if (StringUtils.isNotBlank(clientId)) return clientId; + clientId = getId(); + return clientId; + } + + public String getClientIpAddress() { + if (StringUtils.isNotBlank(clientIpAddress)) return clientIpAddress; + clientIpAddress = ((InetSocketAddress) getSession().getIoSession().getRemoteAddress()).getAddress().getHostAddress(); + return clientIpAddress; + } + + public String getClientHostname() { + if (StringUtils.isNotBlank(clientHostname)) return clientHostname; + clientHostname = ((InetSocketAddress) getSession().getIoSession().getRemoteAddress()).getAddress().getHostName(); + return clientHostname; + } + + public String getClientCanonicalHostname() { + if (StringUtils.isNotBlank(clientCanonicalHostname)) return clientCanonicalHostname; + clientCanonicalHostname = ((InetSocketAddress) getSession().getIoSession().getRemoteAddress()).getAddress().getCanonicalHostName(); + return clientCanonicalHostname; + } + + public int getClientPort() { + if (clientPort > 0) return clientPort; + clientPort = ((InetSocketAddress) getSession().getIoSession().getRemoteAddress()).getPort(); + return clientPort; + } + + public String getClientProperty(@NonNull String propertyName) { return clientProperties.getProperty(propertyName); } + public String getClientProperty(@NonNull String propertyName, String defaultValue) { return clientProperties.getProperty(propertyName, defaultValue); } + + public NodeRegistryEntry getNodeRegistryEntry() { + if (nodeRegistryEntry!=null) + return nodeRegistryEntry; + + //XXX:BUG: Following code seems not working... + String clientId = getClientId(); + if (StringUtils.isNotBlank(clientId)) { + return nodeRegistry.getNodeByClientId(clientId); + } + return null; + } + + public void sendToClient(String msg) { + sendToClient(msg, Level.INFO); + } + + public void sendToClient(String msg, Level logLevel) { + if (msg == null || (msg = msg.trim()).isEmpty()) return; + switch (logLevel) { + case TRACE -> log.trace("{}==> PUSH : {}", id, msg); + case DEBUG -> log.debug("{}==> PUSH : {}", id, msg); + case WARN -> log.warn("{}==> PUSH : {}", id, msg); + case ERROR -> log.error("{}==> PUSH : {}", id, msg); + default -> log.info("{}==> PUSH : {}", id, msg); + } + out.println(msg); + } + + public void sendCommand(String cmd) { + sendToClient(cmd); + } + + public void sendCommand(String cmd, Level logLevel) { + sendToClient(cmd, logLevel); + } + + public void sendCommand(String[] cmd) { + sendToClient(String.join(" ", cmd)); + } + + public void sendCommand(String[] cmd, Level logLevel) { + sendToClient(String.join(" ", cmd), logLevel); + } + + public Object readFromClient(String cmd, Level logLevel) { + String uuid = UUID.randomUUID().toString(); + log.trace("ClientShellCommand.readFromClient: uuid={}, cmd={}", uuid, cmd); + Object oldValue = inputsMap.remove(uuid); + log.trace("ClientShellCommand.readFromClient: uuid={}, old-inputMap-value={}", uuid, oldValue); + log.trace("ClientShellCommand.readFromClient: uuid={}, inputMap-BEFORE={}", uuid, inputsMap); + sendCommand(cmd+" "+uuid, logLevel); + log.trace("ClientShellCommand.readFromClient: uuid={}, Command sent to client", uuid); + while (!inputsMap.containsKey(uuid)) { + log.trace("ClientShellCommand.readFromClient: uuid={}, No input, waiting 500ms", uuid); + try { Thread.sleep(INPUT_CHECK_DELAY); } catch (InterruptedException e) { } + } + log.trace("ClientShellCommand.readFromClient: uuid={}, inputMap-BEFORE={}", uuid, inputsMap); + Object input = inputsMap.remove(uuid); + log.trace("ClientShellCommand.readFromClient: uuid={}, Input found: {}", uuid, input); + return input; + } + + protected String _propertiesToBase64(Properties params) { + if (params != null && params.size() > 0) { + StringWriter writer = new StringWriter(); + try { + params.store(writer, null); + } catch (IOException e) { + log.error("Could not serialize parameters: ", e); + } + String paramsStr = writer.getBuffer().toString(); + return Base64.getEncoder().encodeToString(paramsStr.getBytes(StandardCharsets.UTF_8)); + } + return null; + } + + public void sendParams(Properties params) { + log.debug("sendParams: id={}, parameters={}", id, params); + String paramsStr = _propertiesToBase64(params); + if (paramsStr != null) { + sendToClient("SET-PARAMS " + paramsStr); + } + } + + /** + * Write an object to a Base64 string. + */ + public static String serializeToString(Serializable o) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(o); + oos.close(); + return Base64.getEncoder().encodeToString(baos.toByteArray()); + } + + /** + * Read the object from Base64 string. + */ + public static Object unserializeFromString(String s) throws IOException, ClassNotFoundException { + byte[] data = Base64.getDecoder().decode(s); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); + Object o = ois.readObject(); + ois.close(); + return o; + } + + public static void sendClientConfigurationToClients(@NonNull ClientConfiguration cc, @NonNull List clients) { + List clientIds = clients.stream().map(ClientShellCommand::getClientId).collect(Collectors.toList()); + log.debug("sendClientConfigurationToClients: clients={}, client-config={}", clientIds, cc); + try { + String ccStr = serializeToString(cc); + log.debug("sendClientConfigurationToClients: Serialization of Client configuration: {}", ccStr); + ccStr = "SET-CLIENT-CONFIG " + ccStr; + for (ClientShellCommand csc : clients) { + log.info("sendClientConfigurationToClients: Sending Client configuration to client: {}", csc.getClientId()); + csc.sendToClient(ccStr); + } + log.info("sendClientConfigurationToClients: Client configuration sent to clients: {}", clientIds); + } catch (IOException ex) { + log.error("sendClientConfigurationToClients: Exception while serializing Client configuration: ", ex); + log.error("sendClientConfigurationToClients: SET-CLIENT-CONFIG command *NOT* sent to clients"); + } + } + + public void sendClientConfiguration(ClientConfiguration cc) { + log.debug("sendClientConfiguration: id={}, client-config={}", id, cc); + try { + String ccStr = serializeToString(cc); + log.info("sendClientConfiguration: Serialization of Client configuration: {}", ccStr); + sendToClient("SET-CLIENT-CONFIG " + ccStr); + } catch (IOException ex) { + log.error("sendClientConfiguration: Exception while serializing Client configuration: ", ex); + log.error("sendClientConfiguration: SET-CLIENT-CONFIG command *NOT* sent to client"); + } + } + + public void sendGroupingConfiguration(String grouping, Map connectionConfigs, BaguetteServer server) { + GroupingConfiguration gc = GroupingConfigurationHelper.newGroupingConfiguration(grouping, connectionConfigs, server); + sendGroupingConfiguration(gc); + } + + public void sendGroupingConfiguration(GroupingConfiguration gc) { + String grouping = gc.getName(); + log.debug("sendGroupingConfiguration: id={}, grouping={}, grouping-config={}", id, grouping, gc); + try { + String allStr = serializeToString(gc); + log.info("sendGroupingConfiguration: Serialization of Grouping configuration for {}: {}", grouping, allStr); + sendToClient("SET-GROUPING-CONFIG " + allStr); + } catch (IOException ex) { + log.error("sendGroupingConfiguration: Exception while serializing Grouping configuration: ", ex); + log.error("sendGroupingConfiguration: SET-GROUPING-CONFIG command *NOT* sent to client"); + } + } + + public void sendConstants(Map constants) { + log.debug("sendConstants: constants={}", constants); + HashMap all = new HashMap<>(); + all.put("constants", constants); + + try { + String allStr = serializeToString(all); + log.info("sendConstants: Serialization of Constants: {}", allStr); + sendToClient("SET-CONSTANTS " + allStr); + } catch (IOException ex) { + log.error("sendConstants: Exception while serializing Constants: ", ex); + log.error("sendConstants: SET-CONSTANTS command *NOT* sent to client"); + } + } + + public void setClientId(String id) { + if (id != null && !id.trim().isEmpty()) + sendToClient("SET-ID " + id.trim()); + } + + public void setRole(String role) { + if (role != null && !role.trim().isEmpty()) sendToClient("SET-ROLE " + role.trim().toUpperCase()); + } + + public void setActiveGrouping(String grouping) { + if (grouping != null && !grouping.trim().isEmpty()) + sendToClient("SET-ACTIVE-GROUPING " + grouping.trim().toUpperCase()); + } + + public void stop(String msg) { + log.info("{}==> STOP : {}", id, msg); + out.println("EXIT " + msg); + if (!callbackCalled.getAndSet(true)) { + callback.onExit(1); + } + } + + public String toString() { + return "ClientShellCommand_" + id; + } + + public String toStringCluster() { + return getClientClusterNodeAddress()+":"+getClientClusterNodePort(); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/GroupingConfigurationHelper.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/GroupingConfigurationHelper.java new file mode 100644 index 0000000..211f5fd --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/GroupingConfigurationHelper.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server; + +import gr.iccs.imu.ems.util.GroupingConfiguration; + +import java.util.Map; + +import static gr.iccs.imu.ems.util.GroupingConfiguration.BrokerConnectionConfig; + +/** + * Baguette Client Configuration creation helper + */ +public class GroupingConfigurationHelper { + public static GroupingConfiguration newGroupingConfiguration(String groupingName, Map connectionConfigs, BaguetteServer server) { + return GroupingConfiguration.builder() + .name( groupingName ) + .properties(null) + .brokerConnections(connectionConfigs) + .eventTypeNames( server.getTopicsForGrouping(groupingName) ) + .rules( server.getRulesForGrouping(groupingName) ) + .connections( server.getTopicConnectionsForGrouping(groupingName) ) + .constants( server.getConstants() ) + .functionDefinitions( server.getFunctionDefinitions() ) + .brokerUsername( server.getBrokerUsername() ) + .brokerPassword( server.getBrokerPassword() ) + .build(); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/NodeRegistry.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/NodeRegistry.java new file mode 100644 index 0000000..154d780 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/NodeRegistry.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Node Registry + */ +@Slf4j +@Service +public class NodeRegistry { + private final Map registry = new LinkedHashMap<>(); + @Getter @Setter + private ServerCoordinator coordinator; + + public synchronized NodeRegistryEntry addNode(Map nodeInfo, String clientId) throws UnknownHostException { + String hostnameOrAddress = getIpAddressFromNodeInfo(nodeInfo); + String ipAddress = hostnameOrAddress; + + // Get IP address from provided hostname or address + Throwable errorObj = null; + try { + log.debug("NodeRegistry.addNode(): Resolving IP address from provided hostname/address: {}", hostnameOrAddress); + InetAddress host = InetAddress.getByName(hostnameOrAddress); + log.trace("NodeRegistry.addNode(): InetAddress for provided hostname/address: {}, InetAddress: {}", hostnameOrAddress, host); + String resolvedIpAddress = host.getHostAddress(); + log.info("NodeRegistry.addNode(): Provided-Address={}, Resolved-IP-Address={}", hostnameOrAddress, resolvedIpAddress); + ipAddress = resolvedIpAddress; + } catch (UnknownHostException e) { + log.error("NodeRegistry.addNode(): EXCEPTION while resolving IP address from provided hostname/address: {}\n", ipAddress, e); + errorObj = e; + //throw e; + } + nodeInfo.put("original-address", hostnameOrAddress); + nodeInfo.put("address", ipAddress); + + // Check if an entry with the same IP address is already registered + NodeRegistryEntry entry = registry.get(ipAddress); + if (entry!=null) { + log.debug("NodeRegistry.addNode(): Node already pre-registered: ip-address={}\nOld Node Info: {}\nNew Node Info: {}", + ipAddress, entry, nodeInfo); + if (coordinator!=null && coordinator.allowAlreadyPreregisteredNode(nodeInfo)) { + log.info("NodeRegistry.addNode(): PREVIOUS NODE INFO WILL BE OVERWRITTEN: ip-address={}\nOld Node Info: {}\nNew Node Info: {}", + ipAddress, entry, nodeInfo); + } else { + log.error("NodeRegistry.addNode(): Node already pre-registered and coordinator does not allow new pre-registration requests to overwrite the existing one: ip-address={}\nOld Node Info: {}\nNew Node Info: {}", + ipAddress, entry, nodeInfo); + throw new IllegalStateException("NODE ALREADY PRE-REGISTERED: "+ipAddress); + } + } + + // Create and register node registry entry + entry = new NodeRegistryEntry(ipAddress, clientId, coordinator.getServer()).nodePreregistration(nodeInfo); + if (errorObj!=null) entry.getErrors().add(errorObj); + nodeInfo.put("baguette-client-id", clientId); + registry.put(ipAddress, entry); + log.debug("NodeRegistry.addNode(): Added info for node at address: {}\nNode info: {}", ipAddress, nodeInfo); + return entry; + } + + public synchronized void removeNode(NodeRegistryEntry nodeEntry) { + String ipAddress = nodeEntry.getIpAddress(); + removeNode(ipAddress); + } + + public synchronized void removeNode(Map nodeInfo) { + String ipAddress = getIpAddressFromNodeInfo(nodeInfo); + removeNode(ipAddress); + } + + public synchronized void removeNode(String ipAddress) { + registry.remove(ipAddress); + log.debug("NodeRegistry.removeNode(): Removed info for node at address: {}", ipAddress); + } + + private String getIpAddressFromNodeInfo(Map nodeInfo) { + Object value = nodeInfo.get("ip-address"); + if (value==null || StringUtils.isBlank(value.toString())) value = nodeInfo.get("address"); + if (value==null || StringUtils.isBlank(value.toString())) value = nodeInfo.get("ip"); + if (value==null || StringUtils.isBlank(value.toString())) return null; + return value.toString(); + } + + public synchronized void clearNodes() { + registry.clear(); + log.debug("NodeRegistry.clearNodes(): Cleared node info registry"); + } + + public NodeRegistryEntry getNodeByAddress(String ipAddress) { + NodeRegistryEntry entry = registry.get(ipAddress); + log.debug("NodeRegistry.getNodeByAddress(): Returning info for node at address: {}\nNode Info: {}", ipAddress, entry); + return entry; + } + + public NodeRegistryEntry getNodeByReference(String ref) { + return registry.values().stream() + .filter(n->n.getReference().equals(ref)) + .findAny().orElse(null); + } + + public NodeRegistryEntry getNodeByClientId(String clientId) { + return registry.values().stream() + .filter(n->n.getClientId().equals(clientId)) + .findAny().orElse(null); + } + + public Collection getNodeAddresses() { + return registry.keySet(); + } + + public Collection getNodes() { + return registry.values(); + } + + public Collection getNodeReferences() { + return registry.values().stream().map(NodeRegistryEntry::getReference).collect(Collectors.toList()); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/NodeRegistryEntry.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/NodeRegistryEntry.java new file mode 100644 index 0000000..1248050 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/NodeRegistryEntry.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import gr.iccs.imu.ems.baguette.server.coordinator.cluster.IClusterZone; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.*; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +public class NodeRegistryEntry { + public enum STATE { PREREGISTERED, IGNORE_NODE, INSTALLING, NOT_INSTALLED, INSTALLED, INSTALL_ERROR, + WAITING_REGISTRATION, REGISTERING, REGISTERED, REGISTRATION_ERROR, DISCONNECTED, EXITING, EXITED, NODE_FAILED + }; + @Getter private final String ipAddress; + @Getter private final String clientId; + @JsonIgnore + private final transient BaguetteServer baguetteServer; + @Getter private String hostname; + @Getter private STATE state = null; + @Getter private Date stateLastUpdate; + @Getter private String reference = UUID.randomUUID().toString(); + @Getter private List errors = new LinkedList<>(); + @JsonIgnore + @Getter private transient Map preregistration = new LinkedHashMap<>(); + @JsonIgnore + @Getter private transient Map installation = new LinkedHashMap<>(); + @JsonIgnore + @Getter private transient Map registration = new LinkedHashMap<>(); + @JsonIgnore + @Getter @Setter private transient IClusterZone clusterZone; + + @JsonIgnore + public BaguetteServer getBaguetteServer() { + return baguetteServer; + } + + public String getNodeId() { + return getPreregistration().get("id"); + } + + public String getNodeAddress() { + return ipAddress!=null ? ipAddress : getPreregistration().get("address"); + } + + public String getNodeIdOrAddress() { + return StringUtils.isNotBlank(getNodeId()) ? getNodeId() : getNodeAddress(); + } + + public String getNodeIdAndAddress() { + return getNodeId()+" @ "+getNodeAddress(); + } + + private void setState(@NonNull STATE s) { + state = s; + stateLastUpdate = new Date(); + } + + public void refreshReference() { reference = UUID.randomUUID().toString(); } + + public NodeRegistryEntry nodePreregistration(Map nodeInfo) { + preregistration.clear(); + preregistration.putAll(StrUtil.deepFlattenMap(nodeInfo)); + setState(STATE.PREREGISTERED); + return this; + } + + public NodeRegistryEntry nodeIgnore(Object nodeInfo) { + installation.clear(); + installation.put("ignore-node", nodeInfo!=null ? nodeInfo.toString() : null); + setState(STATE.IGNORE_NODE); + return this; + } + + public NodeRegistryEntry nodeInstalling(Object nodeInfo) { + installation.clear(); + installation.put("installation-task", nodeInfo!=null ? nodeInfo.toString() : "INSTALLING"); + setState(STATE.INSTALLING); + return this; + } + + public NodeRegistryEntry nodeNotInstalled(Object nodeInfo) { + installation.clear(); + installation.put("installation-task-result", nodeInfo!=null ? nodeInfo.toString() : "NOT_INSTALLED"); + setState(STATE.NOT_INSTALLED); + return this; + } + + public NodeRegistryEntry nodeInstallationComplete(Object nodeInfo) { + installation.put("installation-task-result", nodeInfo!=null ? nodeInfo.toString() : "SUCCESS"); + setState(STATE.INSTALLED); + return this; + } + + public NodeRegistryEntry nodeInstallationError(Object nodeInfo) { + installation.put("installation-task-result", nodeInfo!=null ? nodeInfo.toString() : "ERROR"); + setState(STATE.INSTALL_ERROR); + return this; + } + + public NodeRegistryEntry nodeRegistering(Map nodeInfo) { + registration.clear(); + registration.putAll(StrUtil.deepFlattenMap(nodeInfo)); + setState(STATE.REGISTERING); + return this; + } + + public NodeRegistryEntry nodeRegistered(Map nodeInfo) { + //registration.clear(); + registration.putAll(StrUtil.deepFlattenMap(nodeInfo)); + setState(STATE.REGISTERED); + return this; + } + + public NodeRegistryEntry nodeRegistrationError(Map nodeInfo) { + registration.putAll(StrUtil.deepFlattenMap(nodeInfo)); + setState(STATE.REGISTRATION_ERROR); + return this; + } + + public NodeRegistryEntry nodeRegistrationError(Throwable t) { + registration.putAll(StrUtil.deepFlattenMap(Collections.singletonMap("exception", t))); + setState(STATE.REGISTRATION_ERROR); + return this; + } + + public NodeRegistryEntry nodeDisconnected(Map nodeInfo) { + registration.putAll(StrUtil.deepFlattenMap(nodeInfo)); + setState(STATE.DISCONNECTED); + return this; + } + + public NodeRegistryEntry nodeDisconnected(Throwable t) { + registration.putAll(StrUtil.deepFlattenMap(Collections.singletonMap("exception", t))); + setState(STATE.DISCONNECTED); + return this; + } + + public NodeRegistryEntry nodeExiting(Map nodeInfo) { + registration.putAll(StrUtil.deepFlattenMap(nodeInfo)); + setState(STATE.EXITING); + return this; + } + + public NodeRegistryEntry nodeExited(Map nodeInfo) { + registration.putAll(StrUtil.deepFlattenMap(nodeInfo)); + setState(STATE.EXITED); + return this; + } + + public NodeRegistryEntry nodeFailed(Map failInfo) { + if (failInfo!=null) + registration.putAll(StrUtil.deepFlattenMap(failInfo)); + setState(STATE.NODE_FAILED); + return this; + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/ServerCoordinator.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/ServerCoordinator.java new file mode 100644 index 0000000..749402b --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/ServerCoordinator.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server; + +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.GroupingConfiguration; + +import java.util.Map; + +import static gr.iccs.imu.ems.util.GroupingConfiguration.BrokerConnectionConfig; + +public interface ServerCoordinator { + default boolean isSupported(TranslationContext tc) { return true; } + + default boolean supportsAggregators() { return false; } + + void initialize(TranslationContext tc, String upperwareGrouping, BaguetteServer server, Runnable callback); + + default void setProperties(Map p) { } + + default boolean processClientInput(ClientShellCommand csc, String line) { return false; } + + BaguetteServer getServer(); + + int getPhase(); + + default boolean allowAlreadyPreregisteredNode(Map nodeInfo) { return true; } + + default boolean allowAlreadyRegisteredNode(ClientShellCommand csc) { return true; } + + default boolean allowNotPreregisteredNode(ClientShellCommand csc) { return true; } + + default void preregister(NodeRegistryEntry entry) { } + + void register(ClientShellCommand c); + + void unregister(ClientShellCommand c); + + void clientReady(ClientShellCommand c); + + void start(); + + void stop(); + + default void sendGroupingConfigurations(Map connectionConfigs, ClientShellCommand c, BaguetteServer server) { + for (String grouping : server.getGroupingNames()) { + GroupingConfiguration gc = GroupingConfigurationHelper.newGroupingConfiguration(grouping, connectionConfigs, server); + c.sendGroupingConfiguration(gc); + } + } + + default BrokerConnectionConfig getGroupingBrokerConfig(String grouping, ClientShellCommand c) { + String brokerUrl = c.getClientBrokerUrl(); + String brokerCert = c.getClientCertificate(); + String username = c.getClientBrokerUsername(); + String password = c.getClientBrokerPassword(); + return new BrokerConnectionConfig(grouping, brokerUrl, brokerCert, username, password); + } + default BrokerConnectionConfig getUpperwareBrokerConfig(BaguetteServer server) { + String brokerUrl = server.getUpperwareBrokerUrl(); + String brokerCert = server.getBrokerCepService().getBrokerCertificate(); + String username = server.getBrokerUsername(); + String password = server.getBrokerPassword(); + return new BrokerConnectionConfig(server.getUpperwareGrouping(), brokerUrl, brokerCert, username, password); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/Sshd.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/Sshd.java new file mode 100644 index 0000000..29e9a6c --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/Sshd.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server; + +import gr.iccs.imu.ems.baguette.server.coordinator.cluster.ClusteringCoordinator; +import gr.iccs.imu.ems.baguette.server.properties.BaguetteServerProperties; +import gr.iccs.imu.ems.util.EventBus; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.text.StringEscapeUtils; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.session.SessionHeartbeatController; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.mina.MinaServiceFactoryFactory; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.slf4j.event.Level; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.security.KeyPair; +import java.security.PublicKey; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Custom SSH server + */ +@Slf4j +public class Sshd { + @Getter private ServerCoordinator coordinator; + private BaguetteServerProperties configuration; + private SshServer sshd; + private String serverPubkey; + private String serverPubkeyFingerprint; + private String serverPubkeyAlgorithm; + private String serverPubkeyFormat; + private KeyPairProvider serverKeyProvider; + + private boolean heartbeatOn; + private long heartbeatPeriod; + + private EventBus eventBus; + @Getter @Setter + private NodeRegistry nodeRegistry; + + public void start(BaguetteServerProperties configuration, ServerCoordinator coordinator, EventBus eventBus, NodeRegistry registry) throws IOException { + log.info("** SSH server **"); + this.coordinator = coordinator; + this.configuration = configuration; + this.eventBus = eventBus; + this.nodeRegistry = registry; + + // Configure SSH server + int port = configuration.getServerPort(); + String serverKeyFilePath = configuration.getServerKeyFile(); + log.info("SSH server: Public IP address: {}", configuration.getServerAddress()); + log.info("SSH server: Starting on port: {}", port); + log.info("SSH server: Server key file: {}", new File(serverKeyFilePath).getAbsolutePath()); + + // Create SSHD and set port + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(port); + + // Setup server's key provider + _loadPubkeyAndFingerprint(); + sshd.setKeyPairProvider(this.serverKeyProvider); + + // Setup server's shell factory (for custom Shell commands) + sshd.setShellFactory(channelSession -> { + ClientShellCommand csc = new ClientShellCommand(coordinator, configuration.isClientAddressOverrideAllowed(), eventBus, nodeRegistry); + //csc.setId( "#-"+System.currentTimeMillis() ); + log.debug("SSH server: Shell Factory: create invoked : New ClientShellCommand id: {}", csc.getId()); + return csc; + }); + + // Setup password authenticator + sshd.setPasswordAuthenticator((username, password, session) -> { + //public boolean authenticate(String username, String password, ServerSession session) + String pwd = Optional.ofNullable(configuration.getCredentials().get(username.trim())).orElse(""); + return pwd.equals(password); + }); + + // Set session timeout + sshd.setSessionHeartbeat(SessionHeartbeatController.HeartbeatType.IGNORE, Duration.ofMillis(configuration.getHeartbeatPeriod())); + //PropertyResolverUtils.updateProperty(sshd, CoreModuleProperties.HEARTBEAT_INTERVAL.getName(), configuration.getHeartbeatPeriod()); + PropertyResolverUtils.updateProperty(sshd, CoreModuleProperties.IDLE_TIMEOUT.getName(), Long.MAX_VALUE); + PropertyResolverUtils.updateProperty(sshd, CoreModuleProperties.SOCKET_KEEPALIVE.getName(), true); + log.debug("SSH server: Set IDLE_TIMEOUT to MAX, and KEEP-ALIVE to true, and HEARTBEAT to {}", configuration.getHeartbeatPeriod()); + + // Explicitly set IO service factory factory to prevent conflict between MINA and Netty options + sshd.setIoServiceFactoryFactory(new MinaServiceFactoryFactory()); + + // Start SSH server and accept connections + sshd.start(); + log.info("SSH server: Ready"); + + // Start application-level heartbeat service (additional to the SSH and Socket heartbeats) + if (configuration.isHeartbeatEnabled()) { + long heartbeatPeriod = configuration.getHeartbeatPeriod(); + startHeartbeat(heartbeatPeriod); + } + + // Start coordinator + coordinator.start(); + } + + public void stop() throws IOException { + // Stop coordinator + coordinator.stop(); + + // Don't accept new connections + log.info("SSH server: Stopping SSH server..."); + sshd.setShellFactory(null); + + // Signal heartbeat service to stop + stopHeartbeat(); + + // Close active client connections + for (ClientShellCommand csc : ClientShellCommand.getActive()) { + csc.stop("Server exits"); + } + + sshd.stop(); + log.info("SSH server: Stopped"); + } + + public void startHeartbeat(long period) { + heartbeatOn = true; + Thread heartbeat = new Thread( + new Runnable() { + private long period; + + public void run() { + log.info("--> Heartbeat: Started: period={}ms", period); + while (heartbeatOn && period > 0) { + try { + Thread.sleep(period); + } catch (InterruptedException ex) { + } + String msg = String.format("Heartbeat %d", System.currentTimeMillis()); + log.debug("--> Heartbeat: {}", msg); + for (ClientShellCommand csc : ClientShellCommand.getActive()) { + csc.sendToClient(msg, Level.DEBUG); + } + } + log.info("--> Heartbeat: Stopped"); + } + + public Runnable setPeriod(long period) { + this.period = period; + return this; + } + } + .setPeriod(period) + ); + heartbeat.setDaemon(true); + heartbeat.start(); + } + + public void stopHeartbeat() { + heartbeatOn = false; + } + + protected void broadcastToClients(String msg) { + for (ClientShellCommand csc : ClientShellCommand.getActive()) { + log.info("SSH server: Sending to {} : {}", csc.getId(), msg); + csc.sendToClient(msg); + } + } + + public void sendToActiveClients(String command) { + for (ClientShellCommand csc : ClientShellCommand.getActive()) { + log.info("SSH server: Sending to client {} : {}", csc.getId(), command); + csc.sendToClient(command); + } + } + + public void sendToClient(String clientId, String command) { + for (ClientShellCommand csc : ClientShellCommand.getActive()) { + if (csc.getId().equals(clientId)) { + log.info("SSH server: Sending to client {} : {}", csc.getId(), command); + csc.sendToClient(command); + } + } + } + + public void sendToActiveClusters(String command) { + if (!(coordinator instanceof ClusteringCoordinator)) return; + ((ClusteringCoordinator)coordinator).getClusters().forEach(cluster -> { + log.info("SSH server: Sending to cluster {} : {}", cluster.getId(), command); + sendToCluster(cluster.getId(), command); + }); + } + + public void sendToCluster(String clusterId, String command) { + if (!(coordinator instanceof ClusteringCoordinator)) return; + ((ClusteringCoordinator)coordinator).getCluster(clusterId).getNodes().forEach(csc -> { + log.info("SSH server: Sending to client {} : {}", csc.getId(), command); + csc.sendToClient(command); + }); + } + + public Object readFromClient(String clientId, String command, Level logLevel) { + log.trace("SSH server: Sending and Reading to/from client {}: {}", clientId, command); + for (ClientShellCommand csc : ClientShellCommand.getActive()) { + log.trace("SSH server: Check CSC: csc-id={}, client={}", csc.getId(), clientId); + if (csc.getId().equals(clientId)) { + log.debug("SSH server: Sending and Reading to/from client {} : {}", csc.getId(), command); + return csc.readFromClient(command, logLevel); + } + } + return null; + } + + public List getActiveClients() { + return ClientShellCommand.getActive().stream() + .map(c -> String.format("%s %s %s:%d", c.getId(), + c.getClientIpAddress(), + c.getClientClusterNodeHostname(), + c.getClientClusterNodePort())) + .sorted() + .collect(Collectors.toList()); + } + + public Map> getActiveClientsMap() { + return ClientShellCommand.getActive().stream() + //.sorted((final ClientShellCommand c1, final ClientShellCommand c2) -> c1.getId().compareTo(c2.getId())) + .collect(Collectors.toMap(ClientShellCommand::getId, c -> { + Map properties = new LinkedHashMap<>(); + //properties.put("id", c.getId()); + properties.put("ip-address", c.getClientIpAddress()); + properties.put("node-hostname", c.getClientClusterNodeHostname()); + properties.put("node-port", Integer.toString(c.getClientClusterNodePort())); + return properties; + })); + } + + public void sendConstants(Map constants) { + for (ClientShellCommand csc : ClientShellCommand.getActive()) { + log.info("SSH server: Sending constants to client {} : {}", csc.getId(), constants); + csc.sendConstants(constants); + } + } + + public String getPublicKey() { + if (serverPubkey==null) _loadPubkeyAndFingerprint(); + return serverPubkey; + } + + public String getPublicKeyFingerprint() { + if (serverPubkeyFingerprint==null) _loadPubkeyAndFingerprint(); + return serverPubkeyFingerprint; + } + + public String getPublicKeyAlgorithm() { + if (serverPubkey==null) _loadPubkeyAndFingerprint(); + return serverPubkeyAlgorithm; + } + + public String getPublicKeyFormat() { + if (serverPubkey==null) _loadPubkeyAndFingerprint(); + return serverPubkeyFormat; + } + + @SneakyThrows + private synchronized void _loadPubkeyAndFingerprint() { + if (serverPubkey!=null) return; + + String serverKeyFilePath = configuration.getServerKeyFile(); + log.debug("_loadPubkeyAndFingerprint(): Server Key file: {}", serverKeyFilePath); + File serverKeyFile = new File(serverKeyFilePath); + + // Create and configure a new SimpleGeneratorHostKeyProvider instance + SimpleGeneratorHostKeyProvider simpleGeneratorHostKeyProvider = + new SimpleGeneratorHostKeyProvider(serverKeyFile.toPath()); + //simpleGeneratorHostKeyProvider.setStrictFilePermissions(true); // 'true' by default + + // Create or load the Baguette server key pair + List keys = simpleGeneratorHostKeyProvider.loadKeys(null); + if (keys.size()!=1) + throw new IllegalArgumentException("Server key file contains 0 or >1 keys: #keys="+keys.size()+", file="+serverKeyFilePath); + KeyPair serverKey = keys.get(0); + PublicKey publicKey = serverKey.getPublic(); + + // Write Baguette server public key as PEM string + StringWriter writer = new StringWriter(); + JcaPEMWriter pemWriter = new JcaPEMWriter(writer); + pemWriter.writeObject(publicKey); + pemWriter.flush(); + + // Store public key PEM and fingerprint for future use + this.serverPubkey = StringEscapeUtils.escapeJson(writer.toString().trim()); + this.serverPubkeyFormat = publicKey.getFormat(); + this.serverPubkeyAlgorithm = publicKey.getAlgorithm(); + this.serverPubkeyFingerprint = KeyUtils.getFingerPrint(publicKey); + this.serverKeyProvider = simpleGeneratorHostKeyProvider; + log.debug("_loadPubkeyAndFingerprint(): Server public key: \n{}", serverPubkey); + log.debug("_loadPubkeyAndFingerprint(): Fingerprint: {}", serverPubkeyFingerprint); + log.debug("_loadPubkeyAndFingerprint(): Algorithm: {}", serverPubkeyAlgorithm); + log.debug("_loadPubkeyAndFingerprint(): Format: {}", serverPubkeyFormat); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/NoopCoordinator.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/NoopCoordinator.java new file mode 100644 index 0000000..cc26d39 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/NoopCoordinator.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator; + +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.baguette.server.ServerCoordinator; +import gr.iccs.imu.ems.baguette.server.properties.BaguetteServerProperties; +import gr.iccs.imu.ems.translate.TranslationContext; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class NoopCoordinator implements ServerCoordinator { + protected BaguetteServer server; + protected BaguetteServerProperties config; + protected Runnable callback; + protected boolean started; + + @Override + public void initialize(final TranslationContext TC, String upperwareGrouping, BaguetteServer server, Runnable callback) { + if (_logInvocation("initialize", null, false)) return; + this.server = server; + this.config = server.getConfiguration(); + this.callback = callback; + } + + @Override + public BaguetteServer getServer() { + return server; + } + + @Override + public void start() { + if (_logInvocation("start", null, false)) return; + started = true; + + if (callback != null) { + log.info("{}: start(): Invoking callback", getClass().getSimpleName()); + callback.run(); + } + } + + @Override + public void stop() { + if (!_logInvocation("stop", null, true)) return; + started = false; + } + + public boolean isStarted() { + return started; + } + + @Override + public int getPhase() { + return -1; + } + + @Override + public synchronized void preregister(NodeRegistryEntry entry) { + _logInvocation("preregister", entry, true); + } + + @Override + public synchronized void register(ClientShellCommand c) { + _logInvocation("register", c, true); + } + + @Override + public synchronized void unregister(ClientShellCommand c) { + _logInvocation("unregister", c, true); + } + + @Override + public synchronized void clientReady(ClientShellCommand c) { + _logInvocation("clientReady", c, true); + } + + protected boolean _logInvocation(String methodName, Object o, boolean checkStarted) { + String className = getClass().getSimpleName(); + String str = (o==null) ? "" : ( + o instanceof ClientShellCommand ? String.format(". CSC: %s", o) : ( + o instanceof NodeRegistryEntry ? String.format(". NRE: %s", o) : + String.format(". Object: %s", o) + ) + ); + if (checkStarted && !started) { + log.warn("{}: {}(): Coordinator has not been started{}", className, methodName, str); + } else + if (!checkStarted && started) { + log.warn("{}: {}(): Coordinator is already running{}", className, methodName, str); + } else { + log.info("{}: {}(): Method invoked{}", className, methodName, str); + } + return started; + } + + public void sleep(long millis) { + try { Thread.sleep(millis); } catch (Exception ignored) { } + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/ServerCoordinatorTimeWin.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/ServerCoordinatorTimeWin.java new file mode 100644 index 0000000..e1199a1 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/ServerCoordinatorTimeWin.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator; + +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.ServerCoordinator; +import gr.iccs.imu.ems.translate.TranslationContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class ServerCoordinatorTimeWin implements ServerCoordinator { + private final ServerCoordinatorTimeWin LOCK = this; + private BaguetteServer server; + private Runnable callback; + private boolean started; + private long registrationWindow; + private boolean registrationWindowEnded; + private Thread timeout; + private int numClients; + private int phase; + private List clients; + private ClientShellCommand broker; + private int readyClients; + private String brokerCfgIpAddressCmd; + private String brokerCfgPortCmd; + + public void initialize(final TranslationContext TC, String upperwareGrouping, BaguetteServer server, Runnable callback) { + this.server = server; + this.registrationWindow = server.getConfiguration().getRegistrationWindow(); + this.callback = callback; + this.clients = new ArrayList<>(); + } + + public BaguetteServer getServer() { + return server; + } + + public void start() { + timeout = new Thread( + new Runnable() { + private long delay; + + public void run() { + log.info("ServerCoordinatorTimeWin: REGISTRATION PERIOD STARTS"); + started = true; + registrationWindowEnded = false; + try { + Thread.sleep(delay); + } catch (InterruptedException ex) { + log.info("ServerCoordinatorTimeWin: INTERRUPTED: Registration stopped"); + return; + } + log.info("ServerCoordinatorTimeWin: REGISTRATION PERIOD ENDS"); + + List registeredIntime; + synchronized (LOCK) { + registeredIntime = new ArrayList<>(clients); + } + if (registeredIntime.size() > 0) { + startPhase1(registeredIntime); + } else { + registrationWindowEnded = true; + log.warn("ServerCoordinatorTimeWin: No clients have been registered"); + log.warn("ServerCoordinatorTimeWin: The first client to register will become BROKER"); + } + } + + public Runnable setDelay(long delay) { + this.delay = delay; + return this; + } + } + .setDelay(registrationWindow) + ); + timeout.setDaemon(true); + timeout.start(); + log.info("ServerCoordinatorTimeWin: START"); + } + + public void stop() { + started = false; + if (timeout.isAlive()) timeout.interrupt(); + } + + public boolean isStarted() { + return started; + } + + public int getPhase() { + return phase; + } + + public synchronized void register(ClientShellCommand c) { + if (!started) return; + //if (phase!=0) return; + clients.add(c); + numClients++; + if (phase == 0 && numClients == 1 && registrationWindowEnded) startPhase1(clients); + else if (phase != 0) { + c.sendToClient(brokerCfgIpAddressCmd); + c.sendToClient(brokerCfgPortCmd); + c.sendToClient("ROLE CLIENT"); + } + log.info("ServerCoordinatorTimeWin: register: {} clients registered", numClients); + } + + public synchronized void unregister(ClientShellCommand c) { + if (!started) return; + //if (phase!=0) return; + clients.remove(c); + numClients--; + log.info("ServerCoordinatorTimeWin: unregister: {} clients registered", numClients); + } + + protected synchronized void startPhase1(List registeredIntime) { + if (phase != 0) return; + log.info("ServerCoordinatorTimeWin: Phase #1"); + phase = 1; + + // Pick a random client for Broker + int howmany = registeredIntime.size(); + int sel = (int) Math.round((howmany - 1) * Math.random()); + if (sel >= howmany) sel = howmany - 1; + broker = registeredIntime.get(sel); + log.info("ServerCoordinatorTimeWin: Client {} will become BROKER", broker.getId()); + + // Push broker IP address to all clients + try { + //java.net.InetSocketAddress brokerSocketAddress = (java.net.InetSocketAddress) broker.getSession().getIoSession().getRemoteAddress(); + //String brokerIpAddress = brokerSocketAddress.getAddress().getHostAddress(); + //int brokerPort = brokerSocketAddress.getPort(); + String brokerIpAddress = broker.getClientIpAddress(); + int brokerPort = broker.getClientPort(); + if (brokerIpAddress == null || brokerIpAddress.trim().isEmpty() || brokerPort <= 0) + throw new Exception("ServerCoordinatorTimeWin: startPhase1(): Unable to get broker IP address or Port: " + broker); + this.brokerCfgIpAddressCmd = String.format("SET-PARAM bin/broker.cfg-template BROKER_IP_ADDR %s bin/broker.cfg", brokerIpAddress); + this.brokerCfgPortCmd = String.format("SET-PARAM bin/broker.cfg-template BROKER_PORT %d bin/broker.cfg", brokerPort); + } catch (Exception ex) { + this.brokerCfgIpAddressCmd = null; + this.brokerCfgPortCmd = null; + log.error("ServerCoordinatorTimeWin: startPhase1(): Error while getting broker IP address and port: {}", broker); + } + + // Signal BROKER to prepare + phase = 2; + broker.sendToClient("ROLE BROKER"); + } + + public synchronized void clientReady(ClientShellCommand c) { + if (getPhase()==2) _brokerReady(c); + else _clientReady(c); + } + + private void _brokerReady(ClientShellCommand c) { + if (!started) return; + if (phase != 2) return; + log.info("ServerCoordinatorTimeWin: Broker is ready"); + phase = 3; + readyClients = 1; + if (readyClients == numClients) { + phase = 4; + signalTopologyReady(); + } else { + Thread runner = new Thread(new Runnable() { + public void run() { + // Signal all clients except broker to prepare + for (ClientShellCommand c : clients) { + if (c != broker) { + c.sendToClient(brokerCfgIpAddressCmd); + c.sendToClient(brokerCfgPortCmd); + c.sendToClient("ROLE CLIENT"); + } + } + } + }); + runner.setDaemon(true); + runner.start(); + } + } + + private void _clientReady(ClientShellCommand c) { + if (!started) return; + if (phase != 3) return; + readyClients++; + log.info("ServerCoordinatorTimeWin: {} of {} clients are ready", readyClients, numClients); + if (readyClients == numClients) { + phase = 4; + signalTopologyReady(); + } + } + + protected void signalTopologyReady() { + if (phase != 4) return; + log.info("ServerCoordinatorTimeWin: Invoking callback"); + phase = 5; + Thread runner = new Thread(new Runnable() { + public void run() { + // Invoke callback + callback.run(); + log.info("ServerCoordinatorTimeWin: FINISHED"); + } + }); + runner.setDaemon(true); + runner.start(); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/ServerCoordinatorWaitAll.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/ServerCoordinatorWaitAll.java new file mode 100644 index 0000000..9e07f2f --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/ServerCoordinatorWaitAll.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator; + +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.ServerCoordinator; +import gr.iccs.imu.ems.translate.TranslationContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.Vector; + +@Slf4j +public class ServerCoordinatorWaitAll implements ServerCoordinator { + private BaguetteServer server; + private Runnable callback; + private int expectedClients; + private int numClients; + private int phase; + private Vector clients; + private ClientShellCommand broker; + private int readyClients; + + public void initialize(final TranslationContext TC, String upperwareGrouping, BaguetteServer server, Runnable callback) { + this.server = server; + this.expectedClients = server.getConfiguration().getNumberOfInstances(); + this.callback = callback; + this.clients = new Vector<>(); + log.info("initialize: Done"); + } + + public BaguetteServer getServer() { + return server; + } + + public void start() { + } + + public void stop() { + } + + public int getPhase() { + return phase; + } + + public synchronized void register(ClientShellCommand c) { + if (phase != 0) return; + clients.add(c); + numClients++; + log.info("ServerCoordinatorWaitAll: {} of {} clients registered", numClients, expectedClients); + if (numClients == expectedClients) { + startPhase1(); + } + } + + public synchronized void unregister(ClientShellCommand c) { + if (phase != 0) return; + clients.remove(c); + numClients--; + } + + protected synchronized void startPhase1() { + if (phase != 0) return; + log.info("ServerCoordinatorWaitAll: Phase #1"); + phase = 1; + Thread runner = new Thread(new Runnable() { + public void run() { + // Pick a random client for Broker + int sel = (int) Math.round((numClients - 1) * Math.random()); + if (sel >= numClients) sel = numClients - 1; + broker = clients.get(sel); + log.info("ServerCoordinatorWaitAll: Client #{} will become BROKER", broker.getId()); + + // Signal BROKER to prepare + phase = 2; + broker.sendToClient("ROLE BROKER"); + } + }); + runner.setDaemon(true); + runner.start(); + } + + public synchronized void clientReady(ClientShellCommand c) { + if (getPhase()==2) _brokerReady(c); + else _clientReady(c); + } + + private void _brokerReady(ClientShellCommand c) { + if (phase != 2) return; + log.info("ServerCoordinatorWaitAll: Broker is ready"); + phase = 3; + readyClients = 1; + if (readyClients == expectedClients) { + phase = 4; + signalTopologyReady(); + } else { + Thread runner = new Thread(new Runnable() { + public void run() { + // Signal all clients except broker to prepare + for (ClientShellCommand c : clients) { + if (c != broker) { + c.sendToClient("ROLE CLIENT"); + } + } + } + }); + runner.setDaemon(true); + runner.start(); + } + } + + private void _clientReady(ClientShellCommand c) { + if (phase != 3) return; + readyClients++; + log.info("ServerCoordinatorWaitAll: {} of {} clients are ready", readyClients, expectedClients); + if (readyClients == expectedClients) { + phase = 4; + signalTopologyReady(); + } + } + + protected void signalTopologyReady() { + if (phase != 4) return; + log.info("ServerCoordinatorWaitAll: Invoking callback"); + phase = 5; + Thread runner = new Thread(new Runnable() { + public void run() { + // Invoke callback + callback.run(); + log.info("ServerCoordinatorWaitAll: FINISHED"); + } + }); + runner.setDaemon(true); + runner.start(); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/TestCoordinator.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/TestCoordinator.java new file mode 100644 index 0000000..9c6337e --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/TestCoordinator.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import lombok.extern.slf4j.Slf4j; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static gr.iccs.imu.ems.util.GroupingConfiguration.BrokerConnectionConfig; + +@Slf4j +public class TestCoordinator extends NoopCoordinator { + @Override + public synchronized void register(ClientShellCommand c) { + if (!_logInvocation("register", c, true)) return; + _do_register(c); + } + + protected synchronized void _do_register(ClientShellCommand c) { + // prepare configuration + Map connCfgMap = new LinkedHashMap<>(); + BrokerConnectionConfig groupingConn = getUpperwareBrokerConfig(server); + connCfgMap.put(server.getUpperwareGrouping(), groupingConn); + log.trace("ClusteringCoordinator: GLOBAL broker config.: {}", groupingConn); + + connCfgMap.put("PER_CLOUD", groupingConn = getGroupingBrokerConfig("PER_CLOUD", c)); + log.trace("TestCoordinator.test(): {} broker config.: {}", "PER_CLOUD", groupingConn); + + // prepare Broker-CEP configuration + log.info("TestCoordinator.test(): --------------------------------------------------"); + log.info("TestCoordinator.test(): Sending grouping configurations..."); + sendGroupingConfigurations(connCfgMap, c, server); + log.info("TestCoordinator.test(): Sending grouping configurations... done"); + + // Set active grouping and send an event + String grouping = "PER_INSTANCE"; + try { + Thread.sleep(500); + } catch (Exception ex) { + } + log.info("TestCoordinator.test(): --------------------------------------------------"); + log.info("TestCoordinator.test(): Setting active grouping: {}", grouping); + c.setActiveGrouping(grouping); + + try { + Thread.sleep(5000); + } catch (Exception ex) { + } + log.info("TestCoordinator.test(): --------------------------------------------------"); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/TwoLevelCoordinator.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/TwoLevelCoordinator.java new file mode 100644 index 0000000..e2efa77 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/TwoLevelCoordinator.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator; + +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.common.selfhealing.SelfHealingManager; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.GROUPING; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static gr.iccs.imu.ems.util.GroupingConfiguration.BrokerConnectionConfig; + +@Slf4j +public class TwoLevelCoordinator extends NoopCoordinator { + private GROUPING globalGrouping; + private GROUPING nodeGrouping; + + @Override + public boolean isSupported(final TranslationContext _TC) { + // Check if there are at least 2 levels in architecture + Set groupings = _TC.getG2R().keySet(); + if (!groupings.contains("GLOBAL")) return false; + return groupings.size()>1; + } + + @Override + public void initialize(final TranslationContext TC, String upperwareGrouping, BaguetteServer server, Runnable callback) { + if (!isSupported(TC)) + throw new IllegalArgumentException("Passed Translation Context is not supported"); + + super.initialize(TC, upperwareGrouping, server, callback); + List groupings = TC.getG2R().keySet().stream() + .map(GROUPING::valueOf) + .sorted() + .collect(Collectors.toList()); + log.debug("TwoLevelCoordinator.initialize(): Groupings: {}", groupings); + this.globalGrouping = groupings.get(0); + this.nodeGrouping = groupings.get(1); + log.info("TwoLevelCoordinator.initialize(): Groupings: top-level={}, node-level={}", + globalGrouping, nodeGrouping); + + // Configure Self-Healing manager + server.getSelfHealingManager().setMode(SelfHealingManager.MODE.ALL); + } + + @Override + public boolean processClientInput(ClientShellCommand csc, String line) { + if (StringUtils.isBlank(line)) return false; + log.info("TwoLevelCoordinator: Client: {} @ {} -- Input: {}", + csc.getId(), csc.getClientIpAddress(), line); + return true; + } + + @Override + public synchronized void register(ClientShellCommand csc) { + if (!_logInvocation("register", csc, true)) return; + + // prepare configuration + Map connCfgMap = new LinkedHashMap<>(); + BrokerConnectionConfig groupingConn = getUpperwareBrokerConfig(server); + connCfgMap.put(server.getUpperwareGrouping(), groupingConn); + log.trace("TwoLevelCoordinator: GLOBAL broker config.: {}", groupingConn); + + // collect client configurations per grouping + for (String groupingName : server.getGroupingNames()) { + groupingConn = getGroupingBrokerConfig(groupingName, csc); + connCfgMap.put(groupingName, groupingConn); + log.trace("TwoLevelCoordinator: {} broker config.: {}", groupingName, groupingConn); + } + + // send grouping configurations to client + log.info("TwoLevelCoordinator: --------------------------------------------------"); + log.info("TwoLevelCoordinator: Sending grouping configurations to client {}...\n{}", csc.getId(), connCfgMap); + sendGroupingConfigurations(connCfgMap, csc, server); + log.info("TwoLevelCoordinator: Sending grouping configurations to client {}... done", csc.getId()); + sleep(500); + + // Set active grouping + String grouping = nodeGrouping.name(); + log.info("TwoLevelCoordinator: --------------------------------------------------"); + log.info("TwoLevelCoordinator: Setting active grouping of client {}: {}", csc.getId(), grouping); + csc.setActiveGrouping(grouping); + log.info("TwoLevelCoordinator: --------------------------------------------------"); + } + + @Override + public synchronized void unregister(ClientShellCommand csc) { + if (!_logInvocation("unregister", csc, true)) return; + log.info("TwoLevelCoordinator: --------------------------------------------------"); + log.info("TwoLevelCoordinator: Client unregistered: {} @ {}", csc.getId(), csc.getClientIpAddress()); + } +} \ No newline at end of file diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/AtLeastTwoZoneManagementStrategy.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/AtLeastTwoZoneManagementStrategy.java new file mode 100644 index 0000000..3093f3c --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/AtLeastTwoZoneManagementStrategy.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import lombok.extern.slf4j.Slf4j; + +/** + * A smarter than default Zone Management Strategy. + * It groups clients based on domain name, or last byte of IP Address. If neither is available it assigns client + * in a new zone identified by a random UUID. + * When a zone contains only one client, no cluster initialization is instructed. + * When a zone contains exactly two clients, they are both initialized as cluster nodes. + * If only one client is left in a zone, it is instructed to leave cluster. + */ +@Slf4j +public class AtLeastTwoZoneManagementStrategy implements IZoneManagementStrategy { + @Override + public void notPreregisteredNode(ClientShellCommand csc) { + log.warn("AtLeastTwoZoneManagementStrategy: Unexpected node connected: {} @ {}", csc.getId(), csc.getClientIpAddress()); + } + + @Override + public void alreadyRegisteredNode(ClientShellCommand csc) { + log.warn("AtLeastTwoZoneManagementStrategy: Node connection from an already registered IP address: {} @ {}", csc.getId(), csc.getClientIpAddress()); + } + + @Override + public synchronized void nodeAdded(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zone) { + if (zone.getNodes().size() < 2) + return; + + if (zone.getNodes().size()==2) { + // Instruct first node to join cluster first (in fact to initialize it) + ClientShellCommand firstNode = zone.getNodes().get(0); + log.info("AtLeastTwoZoneManagementStrategy: First node to join cluster: client={}, zone={}", firstNode.getId(), zone.getId()); + joinToCluster(firstNode, coordinator, zone); + } + + // Instruct new node to join cluster + log.info("AtLeastTwoZoneManagementStrategy: Node to join cluster: client={}, zone={}", csc.getId(), zone.getId()); + joinToCluster(csc, coordinator, zone); + + // Instruct aggregator election if at least 2 nodes are present in the zone + if (zone.getNodes().size()==2) { + log.info("AtLeastTwoZoneManagementStrategy: Elect aggregator: zone={}", zone.getId()); + coordinator.sleep(5000); + coordinator.electAggregator(zone); + } + } + + private void joinToCluster(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zone) { + coordinator.sendClusterKey(csc, zone); + coordinator.instructClusterJoin(csc, zone, false); + + coordinator.sleep(1000); + csc.sendCommand("CLUSTER-EXEC broker list"); + //coordinator.sleep(1000); + //csc.sendCommand("CLUSTER-TEST"); + } + + @Override + public synchronized void nodeRemoved(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zone) { + // Instruct node to leave cluster + log.info("AtLeastTwoZoneManagementStrategy: Node to leave cluster: client={}, zone={}", csc.getId(), zone.getId()); + coordinator.instructClusterLeave(csc, zone); + + if (zone.getNodes().size()==1) { + // Instruct last node to leave cluster (and terminate cluster) + ClientShellCommand lastNode = zone.getNodes().get(0); + log.info("AtLeastTwoZoneManagementStrategy: Last node to leave cluster: client={}, zone={}", lastNode.getId(), zone.getId()); + coordinator.instructClusterLeave(lastNode, zone); + } + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterSelfHealing.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterSelfHealing.java new file mode 100644 index 0000000..5480a3d --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterSelfHealing.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.common.selfhealing.SelfHealingManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@RequiredArgsConstructor +public class ClusterSelfHealing { + private final SelfHealingManager selfHealingManager; + + // ------------------------------------------------------------------------ + // Server-side self-healing methods + // ------------------------------------------------------------------------ + + List getAggregatorCapableNodesInZone(IClusterZone zone) { + // Get the normal nodes in the zone that can be Aggregators (i.e. Aggregator and candidates) + List aggregatorCapableNodes = zone.findAggregatorCapableNodes(); + if (log.isTraceEnabled()) { + log.trace("getAggregatorCapableNodesInZone: nodes={}", zone.getNodes().stream().map(ClientShellCommand::getNodeRegistryEntry).collect(Collectors.toList())); + log.trace("getAggregatorCapableNodesInZone: aggregatorCapableNodes={}", aggregatorCapableNodes); + } + return aggregatorCapableNodes; + } + + void updateNodesSelfHealingMonitoring(IClusterZone zone, List aggregatorCapableNodes) { + if (aggregatorCapableNodes.size()>1) { + // If zone has >1 aggregator-capable nodes (i.e. Aggregator and Candidates) then stop monitoring them for server-side self-healing + // Aggregator will monitor them for client-side self-healing + List nodes = zone.getNodes().stream().map(ClientShellCommand::getNodeRegistryEntry).collect(Collectors.toList()); + log.info("updateNodesSelfHealingMonitoring: Stop self-healing monitor for zone nodes: zone={}, clients={}", + zone.getId(), nodes.stream().map(NodeRegistryEntry::getIpAddress).collect(Collectors.toList())); + selfHealingManager.removeAllNodes(nodes); + } else if (aggregatorCapableNodes.size()==1) { + // If zone has exactly 1 aggregator-capable node (i.e. Aggregator) then start monitoring it for server-side self-healing + // If Aggregator fails then EMS server must recover it + NodeRegistryEntry lastNode = aggregatorCapableNodes.get(0); + log.info("updateNodesSelfHealingMonitoring: Start self-healing monitor for the first/last node of zone: zone={}, client={}, address={}", zone.getId(), lastNode.getClientId(), lastNode.getIpAddress()); + selfHealingManager.addNode(lastNode); + } + } + + void removeResourceLimitedNodeSelfHealingMonitoring(IClusterZone zone, List aggregatorCapableNodes) { + // Remove self-healing responsibility of RL nodes from EMS server, if there are aggregator-capable nodes in the zone (since one will be/become Aggregator) + List clientlessNodes = zone.getNodesWithoutClient(); + log.trace("removeResourceLimitedNodeSelfHealingMonitoring: AC-nodes: {}", aggregatorCapableNodes); + log.trace("removeResourceLimitedNodeSelfHealingMonitoring: RL-nodes: {}", clientlessNodes); + if (! clientlessNodes.isEmpty() && ! aggregatorCapableNodes.isEmpty()) { + if (log.isTraceEnabled()) { + log.trace("removeResourceLimitedNodeSelfHealingMonitoring: Zone has aggregators-capable node(s) and nodes without client: zone={}, nodes-without-client={}, aggregator-capable-nodes={}", + zone.getId(), clientlessNodes.stream().map(NodeRegistryEntry::getIpAddress).collect(Collectors.toList()), + aggregatorCapableNodes.stream().map(NodeRegistryEntry::getIpAddress).collect(Collectors.toList())); + } + + boolean containsNodesWithoutClient = selfHealingManager.containsAny(zone.getNodesWithoutClient()); + log.trace("removeResourceLimitedNodeSelfHealingMonitoring: containsAny={}", containsNodesWithoutClient); + if (containsNodesWithoutClient) { + // Remove RL nodes self-healing responsibility from EMS server + List zoneNodesWithoutClient = zone.getNodesWithoutClient().stream().map(NodeRegistryEntry::getIpAddress).collect(Collectors.toList()); + log.info("removeResourceLimitedNodeSelfHealingMonitoring: Zone has nodes without client. Will remove self-healing responsibility from EMS server: {}", zoneNodesWithoutClient); + selfHealingManager.removeAllNodes(zone.getNodesWithoutClient()); + log.debug("removeResourceLimitedNodeSelfHealingMonitoring: Removed self-healing responsibility from EMS server, for zone nodes without client: {}", zoneNodesWithoutClient); + } else { + log.trace("removeResourceLimitedNodeSelfHealingMonitoring: No nodes without client have been assigned to EMS server: zone={}", zone.getId()); + } + } + } + + void addResourceLimitedNodeSelfHealingMonitoring(IClusterZone zone, List aggregatorCapableNodes) { + // Add self-healing responsibility of RL nodes to EMS server, if there are no aggregator-capable nodes in the zone + List clientlessNodes = zone.getNodesWithoutClient(); + log.trace("addResourceLimitedNodeSelfHealingMonitoring: AC-nodes: {}", aggregatorCapableNodes); + log.trace("addResourceLimitedNodeSelfHealingMonitoring: RL-nodes: {}", clientlessNodes); + if (! clientlessNodes.isEmpty() && aggregatorCapableNodes.isEmpty()) { + if (log.isTraceEnabled()) { + log.trace("addResourceLimitedNodeSelfHealingMonitoring: Zone has no aggregator-capable nodes but it has nodes without client: zone={}, nodes-without-client={}, aggregator-capable-nodes={}", + zone.getId(), clientlessNodes.stream().map(NodeRegistryEntry::getIpAddress).collect(Collectors.toList()), + aggregatorCapableNodes.stream().map(NodeRegistryEntry::getIpAddress).collect(Collectors.toList())); + } + + // Add RL nodes self-healing responsibility to EMS server + List zoneNodesWithoutClient = zone.getNodesWithoutClient().stream().map(NodeRegistryEntry::getIpAddress).collect(Collectors.toList()); + log.info("removeNodeFromTopology: Zone has only members without client. Will move self-healing responsibility to EMS server: {}", zoneNodesWithoutClient); + selfHealingManager.addAllNodes(zone.getNodesWithoutClient()); + log.debug("removeNodeFromTopology: Moved self-healing responsibility to EMS server, for nodes without client: {}", zoneNodesWithoutClient); + } + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterZone.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterZone.java new file mode 100644 index 0000000..5d9cdc9 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterZone.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.util.ClientConfiguration; +import gr.iccs.imu.ems.util.KeystoreUtil; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.operator.OperatorCreationException; + +import javax.validation.constraints.NotBlank; +import java.io.File; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +@Slf4j +@Data +public class ClusterZone implements IClusterZone { + private final String id; + private final int startPort; + private final int endPort; + + @Getter(AccessLevel.NONE) + private final AtomicInteger currentPort = new AtomicInteger(1200); + @Getter(AccessLevel.NONE) + private final Map nodes = new LinkedHashMap<>(); + @Getter(AccessLevel.NONE) + private final Map addressPortCache = new HashMap<>(); + @Getter(AccessLevel.NONE) + private final Map nodesWithoutClient = new LinkedHashMap<>(); + + private final String clusterId; + private final String clusterKeystoreBase64; + private final File clusterKeystoreFile; + private final String clusterKeystoreType; + private final String clusterKeystorePassword; + @Getter @Setter + private ClientShellCommand aggregator; + + public ClusterZone(@NotBlank String id, int startPort, int endPort, String keystoreFileName) + throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, OperatorCreationException + { + checkArgs(id, startPort, endPort); + this.id = id; + this.startPort = startPort; + this.endPort = endPort; + currentPort.set(startPort); + + this.clusterId = RandomStringUtils.randomAlphanumeric(64); + this.clusterKeystoreFile = new File(keystoreFileName); + this.clusterKeystoreType = "JKS"; + this.clusterKeystorePassword = RandomStringUtils.randomAlphanumeric(64); + log.info("New ClusterZone: zone: {}", id); + log.info(" file: {}", clusterKeystoreFile); + log.info(" type: {}", clusterKeystoreType); + log.debug(" password: {}", PasswordUtil.getInstance().encodePassword(clusterKeystorePassword)); + + log.trace("ClusterZone.: Cluster Keystore: file={}, type={}, pass={}", clusterKeystoreFile.getCanonicalPath(), clusterKeystoreType, clusterKeystorePassword); + log.trace("ClusterZone.: Cluster Id: {}", clusterId); + this.clusterKeystoreBase64 = KeystoreUtil + .getKeystore(clusterKeystoreFile.getCanonicalPath(), clusterKeystoreType, clusterKeystorePassword) + .createIfNotExist() + .createKeyAndCert(clusterId, "CN=" + clusterId, "") + .readFileAsBase64(); + log.debug(" Base64 content: {}", + StringUtils.isNotBlank(clusterKeystoreBase64) ? "Not empty" : "!!! Empty !!!"); + if (log.isTraceEnabled()) + log.trace("ClusterZone.: Cluster Keystore: Base64: {}", PasswordUtil.getInstance().encodePassword(clusterKeystoreBase64)); + } + + private void checkArgs(String id, int startPort, int endPort) { + if (StringUtils.isBlank(id)) + throw new IllegalArgumentException("Zone id cannot be null or blank: zone-id="+id); + if (startPort<1 || endPort<1 || startPort>65535 || endPort>65535) + throw new IllegalArgumentException("Zone start/end port must be between 1 and 65535: zone-id="+id+", start="+startPort+", end="+endPort); + if (startPort > endPort) + throw new IllegalArgumentException("Zone start port must be less than or equal to end port: zone-id="+id+", start="+startPort+", end="+endPort); + } + + public int getPortForAddress(String address) { + return addressPortCache.computeIfAbsent(address, k -> { + int port = currentPort.incrementAndGet(); + if (port>endPort) + throw new IllegalStateException("Zone ports exhausted: "+id); + log.debug("Mapped address-to-port: {} -> {}", address, port); + return port; + }); + } + + public void clearAddressToPortCache() { + addressPortCache.clear(); + } + + // Nodes management + public void addNode(@NonNull ClientShellCommand csc) { + synchronized (Objects.requireNonNull(csc)) { + nodes.put(csc.getClientIpAddress(), csc); + csc.setClientZone(this); + csc.getNodeRegistryEntry().setClusterZone(this); + } + } + + public void removeNode(@NonNull ClientShellCommand csc) { + synchronized (Objects.requireNonNull(csc)) { + nodes.remove(csc.getClientIpAddress()); + if (csc.getClientZone()==this) + csc.setClientZone(null); + if (csc.getNodeRegistryEntry()!=null && csc.getNodeRegistryEntry().getClusterZone()==this) + csc.getNodeRegistryEntry().setClusterZone(null); + if (aggregator==csc) + setAggregator(null); + } + } + + public Set getNodeAddresses() { + return new HashSet<>(nodes.keySet()); + } + + public List getNodes() { + return new ArrayList<>(nodes.values()); + } + + public ClientShellCommand getNodeByAddress(String address) { + return nodes.get(address); + } + + public List findAggregatorCapableNodes() { + return this.nodes.values().stream() + .filter(Objects::nonNull) + .map(ClientShellCommand::getNodeRegistryEntry) + .filter(Objects::nonNull) + .filter(entry -> entry.getState()==NodeRegistryEntry.STATE.REGISTERED || entry.getState()==NodeRegistryEntry.STATE.REGISTERING) + .collect(Collectors.toList()); + } + + // Nodes-without-Clients management + public void addNodeWithoutClient(@NonNull NodeRegistryEntry entry) { + synchronized (Objects.requireNonNull(entry)) { + String address = entry.getIpAddress(); + if (address == null) address = entry.getNodeAddress(); + if (address == null) throw new IllegalArgumentException("Node address not found in Preregistration info"); + nodesWithoutClient.put(address, entry); + entry.setClusterZone(this); + sendClientConfigurationToZoneClients(); + } + } + + public void removeNodeWithoutClient(@NonNull NodeRegistryEntry entry) { + synchronized (Objects.requireNonNull(entry)) { + String address = entry.getIpAddress(); + if (address == null) address = entry.getNodeAddress(); + if (address == null) throw new IllegalArgumentException("Node address not found in Preregistration info"); + nodesWithoutClient.remove(address); + if (entry.getClusterZone() == this) + entry.setClusterZone(null); + sendClientConfigurationToZoneClients(); + } + } + + public Set getNodeWithoutClientAddresses() { + return new HashSet<>(nodesWithoutClient.keySet()); + } + + public List getNodesWithoutClient() { + return new ArrayList<>(nodesWithoutClient.values()); + } + + public NodeRegistryEntry getNodeWithoutClientByAddress(String address) { + return nodesWithoutClient.get(address); + } + + public ClientConfiguration getClientConfiguration() { + return ClientConfiguration.builder() + .nodesWithoutClient(new HashSet<>(nodesWithoutClient.keySet())) + .build(); + } + + public ClientConfiguration sendClientConfigurationToZoneClients() { + ClientConfiguration cc = ClientConfiguration.builder() + .nodesWithoutClient(new HashSet<>(nodesWithoutClient.keySet())) + .build(); + ClientShellCommand.sendClientConfigurationToClients(cc , getNodes()); + return cc; + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterZoneDetector.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterZoneDetector.java new file mode 100644 index 0000000..7974094 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusterZoneDetector.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.springframework.context.expression.MapAccessor; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Detects the Cluster/Zone the given node must be added, + * using node's pre-registration info and a set of configured rules + */ +@Slf4j +public class ClusterZoneDetector implements IClusterZoneDetector { + private final static List DEFAULT_ZONE_DETECTION_RULES = Arrays.asList( + "'${zone:-}'", + "'${zone-id:-}'", + "'${region:-}'", + "'${region-id:-}'", + "'${cloud:-}'", + "'${cloud-id:-}'", + "'${provider:-}'", + "'${provider-id:-}'", + "T(java.time.OffsetDateTime).now().toString()", +// "'Cluster_'+T(java.lang.System).currentTimeMillis()", +// "'Cluster_'+T(java.util.UUID).randomUUID()", + "" + ); + private final static RULE_TYPE DEFAULT_RULES_TYPE = RULE_TYPE.SPEL; + private final static List DEFAULT_ZONES = Collections.singletonList("DEFAULT_CLUSTER"); + private final static ASSIGNMENT_TO_DEFAULT_CLUSTERS DEFAULT_ASSIGNMENT_TO_DEFAULT_CLUSTERS = ASSIGNMENT_TO_DEFAULT_CLUSTERS.RANDOM; + + enum RULE_TYPE { SPEL, MAP } + enum ASSIGNMENT_TO_DEFAULT_CLUSTERS { RANDOM, SEQUENTIAL } + + private RULE_TYPE clusterDetectionRulesType = DEFAULT_RULES_TYPE; + private List clusterDetectionRules = DEFAULT_ZONE_DETECTION_RULES; + private List defaultClusters = DEFAULT_ZONES; + private ASSIGNMENT_TO_DEFAULT_CLUSTERS assignmentToDefaultClusters = DEFAULT_ASSIGNMENT_TO_DEFAULT_CLUSTERS; + + private SpelExpressionParser parser = new SpelExpressionParser(); + private AtomicInteger currentDefaultCluster = new AtomicInteger(0); + + @Override + public void setProperties(Map zoneConfig) { + log.debug("ClusterZoneDetector: setProperties: BEGIN: config: {}", zoneConfig); + + // Get rules type (Map keys or SpEL expressions) + RULE_TYPE rulesType = RULE_TYPE.valueOf( + zoneConfig.getOrDefault("cluster-detector-rules-type", DEFAULT_RULES_TYPE.toString()).toUpperCase()); + + // Get rules texts and separator + String separator = zoneConfig.getOrDefault("cluster-detector-rules-separator", ","); + String rulesStr = zoneConfig.getOrDefault("cluster-detector-rules", null); + if (StringUtils.isNotBlank(rulesStr)) { + List rulesList = Arrays.stream(rulesStr.split(separator)) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .map(String::trim) + .collect(Collectors.toList()); + clusterDetectionRules = (rulesList.size()>0) ? rulesList : DEFAULT_ZONE_DETECTION_RULES; + clusterDetectionRulesType = (rulesList.size()>0) ? rulesType : DEFAULT_RULES_TYPE; + } + + // Get the default cluster(s) + List defaultsList = Arrays.stream(zoneConfig.getOrDefault("default-clusters", "").split(",")) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); + defaultClusters = (defaultsList.size()>0) ? defaultsList : DEFAULT_ZONES; + + // Get assignment method to default clusters + assignmentToDefaultClusters = ASSIGNMENT_TO_DEFAULT_CLUSTERS.valueOf( + zoneConfig.getOrDefault("assignment-to-default-clusters", DEFAULT_ASSIGNMENT_TO_DEFAULT_CLUSTERS.toString().toUpperCase())); + + log.debug("ClusterZoneDetector: setProperties: clusterDetectionRulesType: {}", clusterDetectionRulesType); + log.debug("ClusterZoneDetector: setProperties: clusterDetectionRules: {}", clusterDetectionRules); + log.debug("ClusterZoneDetector: setProperties: defaultClusters: {}", defaultClusters); + log.debug("ClusterZoneDetector: setProperties: assignmentToDefaultClusters: {}", assignmentToDefaultClusters); + } + + @Override + public String getZoneIdFor(ClientShellCommand csc) { + log.trace("ClusterZoneDetector: getZoneIdFor: BEGIN: CSC: {}", csc); + return csc.getClientZone()==null || StringUtils.isBlank(csc.getClientZone().getId()) + ? getZoneIdFor(csc.getNodeRegistryEntry()) + : csc.getClientZone().getId(); + } + + @Override + public String getZoneIdFor(NodeRegistryEntry entry) { + log.trace("ClusterZoneDetector: getZoneIdFor: BEGIN: NRE: {}", entry); + final Map info = entry.getPreregistration(); + + // Select and initialize the right valueMapper based on rules type + log.trace("ClusterZoneDetector: getZoneIdFor: PREREGISTRATION-INFO: {}", info); + Function valueMapper; + switch (clusterDetectionRulesType) { + case SPEL: + StandardEvaluationContext context = new StandardEvaluationContext(info); + context.addPropertyAccessor(new MapAccessor()); + valueMapper = expression -> { + log.trace("ClusterZoneDetector: getZoneIdFor: Expression: {}", expression); + expression = StringSubstitutor.replace(expression, info); + expression = StringSubstitutor.replaceSystemProperties(expression); + log.trace("ClusterZoneDetector: getZoneIdFor: SpEL expr.: {}", expression); + String result = parser.parseRaw(expression).getValue(context, String.class); + log.trace("ClusterZoneDetector: getZoneIdFor: Result: {}", result); + return StringUtils.isBlank(result) ? null : result.trim(); + }; + break; + case MAP: + valueMapper = info::get; + break; + default: + throw new IllegalArgumentException("Unsupported RULE_TYPE: "+ clusterDetectionRulesType); + } + + // Process rules one-by-one, using valueMapper, until one rule yields a non-blank value + String zoneId = clusterDetectionRules.stream() + .filter(StringUtils::isNotBlank) + .peek(s -> log.trace("ClusterZoneDetector: getZoneIdFor: RULE: {}", s)) + .map(valueMapper) + .peek(s -> log.trace("ClusterZoneDetector: getZoneIdFor: RESULT: {}", s)) + .filter(StringUtils::isNotBlank) + .findFirst() + .orElse(null); + log.debug("ClusterZoneDetector: getZoneIdFor: Intermediate: zoneId: {}", zoneId); + + // If all rules yielded blank values then a default cluster id will be selected, using the assignment method + if (StringUtils.isBlank(zoneId)) { + switch (assignmentToDefaultClusters) { + case RANDOM: + zoneId = defaultClusters.get((int) (Math.random() * defaultClusters.size())); + break; + case SEQUENTIAL: + zoneId = defaultClusters.get(currentDefaultCluster.getAndUpdate(operand -> (operand + 1) % defaultClusters.size())); + break; + default: + throw new IllegalArgumentException("Unsupported ASSIGNMENT_TO_DEFAULT_CLUSTERS: "+assignmentToDefaultClusters); + } + } + log.debug("ClusterZoneDetector: getZoneIdFor: END: zoneId: {}", zoneId); + return zoneId; + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusteringCoordinator.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusteringCoordinator.java new file mode 100644 index 0000000..0081575 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/ClusteringCoordinator.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.baguette.server.coordinator.NoopCoordinator; +import gr.iccs.imu.ems.common.selfhealing.SelfHealingManager; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.ClientConfiguration; +import gr.iccs.imu.ems.util.GROUPING; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; + +import java.util.*; +import java.util.stream.Collectors; + +import static gr.iccs.imu.ems.util.GroupingConfiguration.BrokerConnectionConfig; + +@Slf4j +public class ClusteringCoordinator extends NoopCoordinator { + private final static String DEFAULT_ZONE = "default_zone"; + + private final Map topologyMap = new HashMap<>(); + + private IClusterZoneDetector clusterZoneDetector; + private IZoneManagementStrategy zoneManagementStrategy; + private int zoneStartPort = 1200; + private int zoneEndPort = 65535; + private String zoneKeystoreFileNameFormatter = "logs/cluster_${TIMESTAMP}_${ZONE_ID}.p12"; + + private GROUPING topLevelGrouping; + private GROUPING aggregatorGrouping; + private GROUPING lastLevelGrouping; + + private final Map ignoredNodes = new LinkedHashMap<>(); + private ClusterSelfHealing clusterSelfHealing; + + public Collection getClusterIdSet() { return topologyMap.keySet(); } + public Collection getClusters() { return topologyMap.values().stream().map(c->(IClusterZone)c).collect(Collectors.toList()); } + public IClusterZone getCluster(String id) { return topologyMap.get(id); } + + @Override + public boolean isSupported(final TranslationContext _TC) { + log.trace("ClusteringCoordinator.isSupported: TC: {}", _TC); + + // Check if it is a 3-level architecture + Set groupings = _TC.getG2R().keySet(); + log.trace("ClusteringCoordinator.isSupported: Groupings: {}", groupings); + log.trace("ClusteringCoordinator.isSupported: Contains GLOBAL: {}", groupings.contains("GLOBAL")); + log.trace("ClusteringCoordinator.isSupported: Num of Levels: {}", groupings.size()); + + if (!groupings.contains("GLOBAL")) return false; + return groupings.size()==3; + } + + @Override + public boolean supportsAggregators() { + return true; + } + + @Override + public void initialize(final TranslationContext TC, String upperwareGrouping, BaguetteServer server, Runnable callback) { + if (!isSupported(TC)) + throw new IllegalArgumentException("Passed Translation Context is not supported"); + + super.initialize(TC, upperwareGrouping, server, callback); + List groupings = TC.getG2R().keySet().stream() + .map(GROUPING::valueOf) + .sorted() + .collect(Collectors.toList()); + log.debug("ClusteringCoordinator.initialize(): Groupings: {}", groupings); + this.topLevelGrouping = groupings.get(0); + this.aggregatorGrouping = groupings.get(1); + this.lastLevelGrouping = groupings.get(2); + log.info("ClusteringCoordinator.initialize(): Groupings: top-level={}, aggregator={}, last-level={}", + topLevelGrouping, aggregatorGrouping, lastLevelGrouping); + + // Configure Self-Healing manager + clusterSelfHealing = new ClusterSelfHealing(server.getSelfHealingManager()); + server.getSelfHealingManager().setMode(SelfHealingManager.MODE.INCLUDED); + } + + @SneakyThrows + public void setProperties(Map zoneConfig) { + log.debug("Zone configuration: {}", zoneConfig); + zoneManagementStrategy = zoneConfig.containsKey("zone-management-strategy-class") + ? (IZoneManagementStrategy) Class.forName(zoneConfig.get("zone-management-strategy-class")).getConstructor().newInstance() + : new DefaultZoneManagementStrategy(); + zoneStartPort = zoneConfig.containsKey("zone-port-start") + ? Integer.parseInt(zoneConfig.get("zone-port-start")) : zoneStartPort; + zoneEndPort = zoneConfig.containsKey("zone-port-end") + ? Integer.parseInt(zoneConfig.get("zone-port-end")) : zoneEndPort; + zoneKeystoreFileNameFormatter = zoneConfig.containsKey("zone-keystore-file-name-formatter") + ? zoneConfig.get("zone-keystore-file-name-formatter") : zoneKeystoreFileNameFormatter; + + // Initialize Cluster Detector + String clusterDetectorClass = zoneConfig.get("cluster-detector-class"); + if (StringUtils.isNotBlank(clusterDetectorClass)) { + Class clazz = Class.forName(clusterDetectorClass); + if (clazz.isAssignableFrom(IClusterZoneDetector.class)) + clusterZoneDetector = (IClusterZoneDetector) clazz.getConstructor().newInstance(); + else + throw new IllegalArgumentException("Invalid Cluster Detector class. Not implementing IClusterZoneDetector interface: "+clazz.getName()); + } else { + clusterZoneDetector = new ClusterZoneDetector(); + } + clusterZoneDetector.setProperties(zoneConfig); + log.info("Cluster Detector class: {}", clusterZoneDetector.getClass().getName()); + } + + @Override + public boolean processClientInput(ClientShellCommand csc, String line) { + if (StringUtils.isBlank(line)) return false; + String[] args = Arrays.stream(line.trim().split("[ \t\r\n]+")).filter(StringUtils::isNotBlank).map(String::trim).toArray(String[]::new); + if (!"CLUSTER".equalsIgnoreCase(args[0])) return false; + if ("AGGREGATOR".equalsIgnoreCase(args[1])) { + String clientId1 = csc.getId(); + String clientId2 = csc.getClientId(); + String clientId3 = args[2]; + log.trace("processClientInput: csc.zone: {}", csc.getClientZone()!=null ? csc.getClientZone().getId() : null); + log.trace("processClientInput: topology-map: {}", topologyMap.keySet()); + ClusterZone zone = findZone(csc); + log.trace("processClientInput: zone={}", zone); + zone.setAggregator(csc); + log.info("Updated aggregator of zone: {} -- New aggregator: {} @ {} ({})", + zone.getId(), clientId1, csc.getClientIpAddress(), clientId2); + } + return true; + } + + private ClusterZone findZone(ClientShellCommand csc) { + String zoneId = clusterZoneDetector.getZoneIdFor(csc); + return topologyMap.get(zoneId); + } + + @Override + public boolean allowAlreadyPreregisteredNode(Map nodeInfo) { + return zoneManagementStrategy.allowAlreadyPreregisteredNode(nodeInfo); + } + + @Override + public boolean allowAlreadyRegisteredNode(ClientShellCommand csc) { + return zoneManagementStrategy.allowAlreadyRegisteredNode(csc); + } + + @Override + public boolean allowNotPreregisteredNode(ClientShellCommand csc) { + return zoneManagementStrategy.allowNotPreregisteredNode(csc); + } + + @Override + public synchronized void preregister(@NonNull NodeRegistryEntry entry) { + log.debug("ClusteringCoordinator: preregister: BEGIN: NRE:\n{}", entry); + + if (!_logInvocation("preregister", entry.getNodeIdAndAddress(), true)) return; + + // Check if client has been preregistered (or connected without being expected) + /*if (zoneManagementStrategy.allowNotPreregisteredNode(entry)) { + log.warn("Non-Preregistered node will be preregistered: {} @ {}", entry.getClientId(), entry.getIpAddress()); + zoneManagementStrategy.notPreregisteredNode(entry); + }*/ + + log.debug("ClusteringCoordinator: preregister: Checking node State: node={}, state={}", entry.getNodeIdAndAddress(), entry.getState()); + if (entry.getState()==NodeRegistryEntry.STATE.IGNORE_NODE) { + // Add in ignored nodes list + log.info("ClusteringCoordinator: preregister: Ignoring node: node={}, state={}", entry.getNodeIdAndAddress(), entry.getState()); + ignoredNodes.put(entry.getIpAddress(), entry); + } else + if (entry.getState()==NodeRegistryEntry.STATE.NOT_INSTALLED) { + // Append to Nodes without EMS client (e.g. Edge devices, resource-limited VM's) + log.debug("ClusteringCoordinator: preregister: Adding node without EMS client: node={}, state={}", entry.getNodeIdAndAddress(), entry.getState()); + + // Assign node-without-client in a zone + String zoneId = clusterZoneDetector.getZoneIdFor(entry); + log.debug("ClusteringCoordinator: preregister: New entry: node={}, zone-id={}", entry.getNodeIdAndAddress(), zoneId); + if (log.isTraceEnabled()) { + log.trace("preregister: topologyMap: BEFORE: keys={}", topologyMap.keySet()); + log.trace("preregister: topologyMap: containsKey: key={}, result={}", zoneId, topologyMap.containsKey(zoneId)); + } + ClusterZone zone = topologyMap.computeIfAbsent(zoneId, this::createClusterZone); + log.trace("ClusteringCoordinator: preregister: Zone members without client: BEFORE: {}", zone.getNodesWithoutClient()); + zone.addNodeWithoutClient(entry); + log.trace("ClusteringCoordinator: preregister: Zone members without client: AFTER: {}", zone.getNodesWithoutClient()); + } else + if (entry.getState()==NodeRegistryEntry.STATE.INSTALLED) { + // Append to normal Node with EMS client + log.debug("ClusteringCoordinator: preregister: Node with EMS client: node={}, state={}", entry.getNodeIdAndAddress(), entry.getState()); + // No need to do something + } else { + // Other states are ignored + log.warn("ClusteringCoordinator: preregister: No preregistration due to node state: node={}, state={}", entry.getNodeIdAndAddress(), entry.getState()); + } + } + + @SneakyThrows + private ClusterZone createClusterZone(@NonNull String id) { + Map values = new HashMap<>(); + values.put("TIMESTAMP", ""+System.currentTimeMillis()); + values.put("ZONE_ID", id.replaceAll("[^A-Za-z0-9_]", "_")); + String keystoreFile = StringSubstitutor.replace(zoneKeystoreFileNameFormatter, values); + return new ClusterZone(id, zoneStartPort, zoneEndPort, keystoreFile); + } + + @Override + public synchronized void register(ClientShellCommand csc) { + if (!_logInvocation("register", csc, true)) return; + + // Check if client has been preregistered (or connected without being expected) + NodeRegistryEntry preregEntry = server.getNodeRegistry().getNodeByAddress(csc.getClientIpAddress()); + log.debug("Preregistered info for node: {} @ {}:\n{}", csc.getId(), csc.getClientIpAddress(), preregEntry); + if (preregEntry==null && zoneManagementStrategy.allowNotPreregisteredNode(csc)) { + log.warn("Non Preregistered node connected: {} @ {}", csc.getId(), csc.getClientIpAddress()); + log.warn("Preregistered nodes: {}", server.getNodeRegistry().getNodes().stream() + .map(entry->entry.getState()+"/"+entry.getIpAddress()+"/"+entry.getNodeIdAndAddress()+"/"+entry.getClientId()) + .collect(Collectors.toList())); + zoneManagementStrategy.notPreregisteredNode(csc); + } else if (preregEntry==null) { + log.warn("Non Preregistered node is refused connection: {} @ {}", csc.getId(), csc.getClientIpAddress()); + csc.setCloseConnection(true); + return; + } + + // Check if client has already been registered (i.e. is still connected) + ClientShellCommand regEntry = topologyMap.values().stream() + .map(zone->zone.getNodeByAddress(csc.getClientIpAddress())) + .filter(Objects::nonNull) + .findAny().orElse(null); + log.debug("Registered CSC for node: {} @ {}:\n{}", csc.getId(), csc.getClientIpAddress(), regEntry); + if (regEntry!=null && allowAlreadyRegisteredNode(csc)) { + log.warn("Already Registered node connected: {} @ {}", csc.getId(), csc.getClientIpAddress()); + zoneManagementStrategy.alreadyRegisteredNode(csc); + } else if (regEntry!=null) { + log.warn("New node is refused connection because an active connection from the same IP address already exists: {} @ {}", csc.getId(), csc.getClientIpAddress()); + csc.setCloseConnection(true); + return; + } + + // Register client + _do_register(csc); + } + + @Override + public synchronized void unregister(ClientShellCommand csc) { + if (!_logInvocation("unregister", csc, true)) return; + _do_unregister(csc); + } + + protected synchronized void _do_register(ClientShellCommand csc) { + // Add registered node in topology map + addNodeInTopology(csc); + + // collect client configuration + ClientConfiguration clientConfig = csc.getClientZone().getClientConfiguration(); + + // prepare configuration + Map connCfgMap = new LinkedHashMap<>(); + BrokerConnectionConfig groupingConn = getUpperwareBrokerConfig(server); + connCfgMap.put(server.getUpperwareGrouping(), groupingConn); + log.trace("ClusteringCoordinator: GLOBAL broker config.: {}", groupingConn); + + // collect client configurations per grouping + for (String groupingName : server.getGroupingNames()) { + groupingConn = getGroupingBrokerConfig(groupingName, csc); + connCfgMap.put(groupingName, groupingConn); + log.trace("ClusteringCoordinator: {} broker config.: {}", groupingName, groupingConn); + } + + // send client configuration to client + log.info("ClusteringCoordinator: --------------------------------------------------"); + log.info("ClusteringCoordinator: Sending client configuration to client {}...\n{}", csc.getId(), clientConfig); + csc.getClientZone().sendClientConfigurationToZoneClients(); + log.info("ClusteringCoordinator: Sending client configuration to client {}... done", csc.getId()); + sleep(500); + + // send grouping configurations to client + log.info("ClusteringCoordinator: --------------------------------------------------"); + log.info("ClusteringCoordinator: Sending grouping configurations to client {}...\n{}", csc.getId(), connCfgMap); + sendGroupingConfigurations(connCfgMap, csc, server); + log.info("ClusteringCoordinator: Sending grouping configurations to client {}... done", csc.getId()); + sleep(500); + + // Set active grouping + String grouping = lastLevelGrouping.name(); + log.info("ClusteringCoordinator: --------------------------------------------------"); + log.info("ClusteringCoordinator: Setting active grouping of client {}: {}", csc.getId(), grouping); + csc.setActiveGrouping(grouping); + log.info("ClusteringCoordinator: --------------------------------------------------"); + sleep(500); + + // Registered node added in topology map - Notify ZoneManagementStrategy + addedNodeInTopology(csc); + } + + private synchronized void addNodeInTopology(ClientShellCommand csc) { + // Assign client in a zone + String zoneId = clusterZoneDetector.getZoneIdFor(csc); + log.debug("addNodeInTopology: New client: id={}, address={}, zone-id={}", csc.getId(), csc.getClientIpAddress(), zoneId); + ClusterZone zone = topologyMap.computeIfAbsent(zoneId, this::createClusterZone); + log.trace("addNodeInTopology: Zone members: BEFORE: {}", zone.getNodes()); + zone.addNode(csc); + log.trace("addNodeInTopology: Zone members: AFTER: {}", zone.getNodes()); + + // Initialize new client's cluster node address/hostname, port and certificate + String nodeAddress = csc.getClientIpAddress(); + String nodeHostname = csc.getClientHostname(); + String nodeCanonical = csc.getClientCanonicalHostname(); + int nodePort = zone.getPortForAddress(nodeAddress); + csc.setClientClusterNodePort(nodePort); + csc.setClientClusterNodeAddress(nodeAddress); + csc.setClientClusterNodeHostname(nodeHostname); + //csc.setClientClusterNodeHostname(nodeCanonical); + log.debug("addNodeInTopology: New client: Cluster node: address={}, hostname={} // {}, port={}", + nodeAddress, nodeHostname, nodeCanonical, nodePort); + } + + private synchronized void addedNodeInTopology(ClientShellCommand csc) { + // Signal Zone Management Strategy for new client registration + zoneManagementStrategy.nodeAdded(csc, this, csc.getClientZone()); + log.info("addNodeInTopology: Client added in topology: client={}, address={}", csc.getId(), csc.getClientIpAddress()); + + if (csc.getClientZone()!=null) { + IClusterZone zone = csc.getClientZone(); + log.trace("addNodeInTopology: CSC is in zone: client={}, address={}, zone={}", csc.getId(), csc.getClientIpAddress(), zone.getId()); + + // Self-healing-related actions + List aggregatorCapableNodes = clusterSelfHealing.getAggregatorCapableNodesInZone(zone); + clusterSelfHealing.updateNodesSelfHealingMonitoring(zone, aggregatorCapableNodes); + clusterSelfHealing.removeResourceLimitedNodeSelfHealingMonitoring(zone, aggregatorCapableNodes); + } + } + + protected synchronized void _do_unregister(ClientShellCommand csc) { + // Remove node from topology map + removeNodeFromTopology(csc); + } + + private synchronized void removeNodeFromTopology(ClientShellCommand csc) { + // Assign client in a zone + String zoneId = csc.getNodeRegistryEntry()!=null ? clusterZoneDetector.getZoneIdFor(csc) : null; + ClusterZone zone = StringUtils.isNotBlank(zoneId) ? topologyMap.get(zoneId) : null; + if (zone == null) { + log.warn("removeNodeFromTopology: Non-registered client removed: client={}, address={}, zone-id={}", csc.getId(), csc.getClientIpAddress(), zoneId); + log.debug("removeNodeFromTopology: Non-registered client removed: entry={}", csc.getNodeRegistryEntry()); + } else { + log.trace("removeNodeFromTopology: Zone members: BEFORE: {}", zone.getNodes()); + zone.removeNode(csc); + log.trace("removeNodeFromTopology: Zone members: AFTER: {}", zone.getNodes()); + zoneManagementStrategy.nodeRemoved(csc, this, zone); + log.info("removeNodeFromTopology: Client removed from topology: client={}, address={}", csc.getId(), csc.getClientIpAddress()); + + ClientShellCommand aggregator = zone.getAggregator(); + if (aggregator==csc || aggregator==null) { + if (aggregator==csc) zone.setAggregator(null); + log.warn("removeNodeFromTopology: Zone without aggregator: zone-id={}, old-aggregator-id={}, address={}", zone.getId(), csc.getId(), csc.getClientIpAddress()); + + // Nothing to do. Client-side self-healing must elect a new Aggregator + // Optionally, we can start a timer so that if no Aggregator is elected within a period, then we can appoint one or trigger Server-side self-healing + } + + // Self-healing-related actions + List aggregatorCapableNodes = clusterSelfHealing.getAggregatorCapableNodesInZone(zone); + clusterSelfHealing.updateNodesSelfHealingMonitoring(zone, aggregatorCapableNodes); + if (aggregatorCapableNodes.isEmpty()) + ; //XXX: TODO: ??Reconfigure non-candidate nodes to forward their events to EMS server?? + clusterSelfHealing.addResourceLimitedNodeSelfHealingMonitoring(zone, aggregatorCapableNodes); + } + } + + // ------------------------------------------------------------------------ + // Methods to be used by Zone Management Strategies + // ------------------------------------------------------------------------ + + void sendClusterKey(ClientShellCommand csc, IClusterZone zoneInfo) { + csc.sendCommand(String.format("CLUSTER-KEY %s %s %s %s", + zoneInfo.getClusterKeystoreFile().getName(), zoneInfo.getClusterKeystoreType(), + zoneInfo.getClusterKeystorePassword(), zoneInfo.getClusterKeystoreBase64())); + } + + void sendCommandToZone(String command, List zoneNodes) { + log.info("sendCommandToZone: Sending command: \"{}\" to zone nodes: {}", command, + zoneNodes.stream().map(ClientShellCommand::toStringCluster).collect(Collectors.toList())); + zoneNodes.forEach(c -> c.sendCommand(command)); + } + + void instructClusterJoin(ClientShellCommand csc, IClusterZone zone, boolean startElection) { + List zoneNodes = zone.getNodes(); + log.debug("instructClusterJoin: Zone members: {}", zoneNodes); + + // Build zone members list + final List addresses = new ArrayList<>(); + final List hostnames = new ArrayList<>(); + zoneNodes.forEach(c -> { + if (c!=csc) { + addresses.add(c.getClientClusterNodeAddress()+":"+c.getClientClusterNodePort()); + hostnames.add(c.getClientClusterNodeHostname()+":"+c.getClientClusterNodePort()); + } + }); + log.debug("instructClusterJoin: New cluster node nearby members: addresses={}, hostnames={}", addresses, hostnames); + + // Prepare cluster join commands + String command = String.format("%s %s:%s:%s start-election=%b %s:%d %s", + zone.getId(), + topLevelGrouping, aggregatorGrouping, lastLevelGrouping, + startElection, + csc.getClientClusterNodeAddress(), + csc.getClientClusterNodePort(), + String.join(" ", addresses)); + /*String command = + zone.getId()+" " + +topLevelGrouping+":"+aggregatorGrouping+":"+lastLevelGrouping+" " + +startElection+" " + +csc.getClientClusterNodeHostname()+":"+csc.getClientClusterNodePort()+" " + +String.join(" ", hostnames);*/ + + // Send cluster join command + log.debug("instructClusterJoin: Client {} @ {} joins cluster: CLUSTER-JOIN {}", csc.getId(), csc.getClientIpAddress(), command); + csc.sendCommand("CLUSTER-JOIN "+command); + } + + void instructClusterLeave(ClientShellCommand csc, IClusterZone zone) { + // Send cluster leave command + log.debug("instructClusterLeave: Client {} @ {} leaves cluster: CLUSTER-LEAVE", csc.getId(), csc.getClientIpAddress()); + try { + csc.sendCommand("CLUSTER-LEAVE"); + } catch (Exception e) { + // Channel has probably already been closed + log.warn("instructClusterLeave: EXCEPTION: ", e); + } + } + + void electAggregator(IClusterZone zone) { + sendCommandToZone("CLUSTER-EXEC broker elect", zone.getNodes()); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/DefaultZoneManagementStrategy.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/DefaultZoneManagementStrategy.java new file mode 100644 index 0000000..1573812 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/DefaultZoneManagementStrategy.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import lombok.extern.slf4j.Slf4j; + +/** + * The default Zone Management Strategy used when 'zone-management-strategy-class' property is not set. + * It groups clients based on domain name, or last byte of IP Address. If neither is available it assigns client + * in a new zone identified by a random UUID. + * The first client to join a zone will be instructed to start cluster and become aggregator. + * Subsequent clients will just join the cluster. + */ +@Slf4j +public class DefaultZoneManagementStrategy implements IZoneManagementStrategy { + @Override + public void notPreregisteredNode(ClientShellCommand csc) { + log.warn("DefaultZoneManagementStrategy: Unexpected node connected: {} @ {}", csc.getId(), csc.getClientIpAddress()); + } + + @Override + public void alreadyRegisteredNode(ClientShellCommand csc) { + log.warn("DefaultZoneManagementStrategy: Node connection from an already registered IP address: {} @ {}", csc.getId(), csc.getClientIpAddress()); + } + + @Override + public synchronized void nodeAdded(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zone) { + // Instruct new node to join cluster + log.info("DefaultZoneManagementStrategy: Node to join cluster: client={}, zone={}", csc.getId(), zone.getId()); + joinToCluster(csc, coordinator, zone); + } + + private void joinToCluster(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zone) { + coordinator.sendClusterKey(csc, zone); + coordinator.instructClusterJoin(csc, zone, true); + + coordinator.sleep(1000); + csc.sendCommand("CLUSTER-EXEC broker list"); + //coordinator.sleep(1000); + //csc.sendCommand("CLUSTER-TEST"); + } + + @Override + public synchronized void nodeRemoved(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zone) { + // Instruct node to leave cluster + log.info("DefaultZoneManagementStrategy: Node to leave cluster: client={}, zone={}", csc.getId(), zone.getId()); + coordinator.instructClusterLeave(csc, zone); + } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IClusterZone.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IClusterZone.java new file mode 100644 index 0000000..bde3749 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IClusterZone.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.util.ClientConfiguration; +import lombok.NonNull; + +import java.io.File; +import java.util.List; +import java.util.Set; + +public interface IClusterZone { + String getId(); + void addNode(@NonNull ClientShellCommand csc); + void removeNode(@NonNull ClientShellCommand csc); + Set getNodeAddresses(); + List getNodes(); + ClientShellCommand getNodeByAddress(String address); + + List findAggregatorCapableNodes(); + + void addNodeWithoutClient(@NonNull NodeRegistryEntry entry); + void removeNodeWithoutClient(@NonNull NodeRegistryEntry entry); + Set getNodeWithoutClientAddresses(); + List getNodesWithoutClient(); + NodeRegistryEntry getNodeWithoutClientByAddress(String address); + + ClientConfiguration getClientConfiguration(); + ClientConfiguration sendClientConfigurationToZoneClients(); + + File getClusterKeystoreFile(); + String getClusterKeystoreType(); + String getClusterKeystorePassword(); + String getClusterKeystoreBase64(); +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IClusterZoneDetector.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IClusterZoneDetector.java new file mode 100644 index 0000000..a03a9d1 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IClusterZoneDetector.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; + +import java.util.Map; + +public interface IClusterZoneDetector { + String getZoneIdFor(ClientShellCommand csc); + String getZoneIdFor(NodeRegistryEntry entry); + void setProperties(Map zoneConfig); +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IZoneManagementStrategy.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IZoneManagementStrategy.java new file mode 100644 index 0000000..56d97e6 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/coordinator/cluster/IZoneManagementStrategy.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.coordinator.cluster; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; + +import java.util.Map; + +public interface IZoneManagementStrategy { + default boolean allowAlreadyPreregisteredNode(Map nodeInfo) { return true; } + default boolean allowAlreadyPreregisteredNode(NodeRegistryEntry entry) { return true; } + default boolean allowAlreadyRegisteredNode(ClientShellCommand csc) { return true; } + default boolean allowAlreadyRegisteredNode(NodeRegistryEntry entry) { return true; } + default boolean allowNotPreregisteredNode(ClientShellCommand csc) { return true; } + default boolean allowNotPreregisteredNode(NodeRegistryEntry entry) { return true; } + default void notPreregisteredNode(ClientShellCommand csc) { } + default void notPreregisteredNode(NodeRegistryEntry entry) { } + default void alreadyRegisteredNode(ClientShellCommand csc) { } + default void alreadyRegisteredNode(NodeRegistryEntry entry) { } + + default void nodeAdded(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zoneInfo) { } + default void nodeRemoved(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zoneInfo) { } +} diff --git a/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/properties/BaguetteServerProperties.java b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/properties/BaguetteServerProperties.java new file mode 100644 index 0000000..831bd37 --- /dev/null +++ b/ems-core/baguette-server/src/main/java/gr/iccs/imu/ems/baguette/server/properties/BaguetteServerProperties.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.baguette.server.properties; + +import gr.iccs.imu.ems.baguette.server.ServerCoordinator; +import gr.iccs.imu.ems.util.CredentialsMap; +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "baguette.server") +public class BaguetteServerProperties implements InitializingBean { + public void afterPropertiesSet() { + log.debug("BaguetteServerProperties: {}", this); + checkConfig(); + } + + private void checkConfig() { + // Check that either coordinator class or id is provided + if (coordinatorClass==null && (coordinatorId==null || coordinatorId.size()==0)) + throw new IllegalArgumentException("Either coordinator class or coordinator id must be provided"); + if (coordinatorId!=null && coordinatorId.size()>0) { + coordinatorId.forEach(id -> { + CoordinatorConfig cc = getCoordinatorConfig().get(id); + if (cc==null) + throw new IllegalArgumentException("Not found coordinator configuration with id: "+id); + if (cc.getCoordinatorClass()==null) + throw new IllegalArgumentException("No coordinator class in configuration with id: "+id); + }); + } + } + + private Class coordinatorClass; + private Map coordinatorParameters = new HashMap<>(); + + private List coordinatorId; + private Map coordinatorConfig = new HashMap<>(); + + @Min(-1) + private long registrationWindow = 30000; + @Min(-1) + private int numberOfInstances = -1; + @Min(-1) + private int NumberOfSegments = -1; + + private String address; + public String getServerAddress() { return address; } + private boolean resolveHostname = true; + + @Min(value = 1, message = "Valid server ports are between 1 and 65535. Please prefer ports higher than 1023.") + @Max(value = 65535, message = "Valid server ports are between 1 and 65535. Please prefer ports higher than 1023.") + private int port = 2222; + public int getServerPort() { return port; } + + private String keyFile = "hostkey.pem"; + public String getServerKeyFile() { return keyFile; } + + private boolean heartbeatEnabled; + @Min(-1) + private long heartbeatPeriod = 60000; + + private boolean clientAddressOverrideAllowed; + private String clientIdFormat; + private String clientIdFormatEscape = "~"; + + private final CredentialsMap credentials = new CredentialsMap(); + + @Data + public static class CoordinatorConfig { + private Class coordinatorClass; + private Map parameters; + } +} diff --git a/ems-core/bin/client.sh b/ems-core/bin/client.sh new file mode 100644 index 0000000..0f8d80c --- /dev/null +++ b/ems-core/bin/client.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) + +#JAVA_OPTS=-Djavax.net.ssl.trustStore=./broker-truststore.p12\ -Djavax.net.ssl.trustStorePassword=melodic\ -Djavax.net.ssl.trustStoreType=pkcs12 +# -Djavax.net.debug=all +# -Djavax.net.debug=ssl,handshake,record + +java $JAVA_OPTS -jar ${BASEDIR}/public_resources/resources/broker-client.jar $* diff --git a/ems-core/bin/cp2cdo.bat b/ems-core/bin/cp2cdo.bat new file mode 100644 index 0000000..9f27c34 --- /dev/null +++ b/ems-core/bin/cp2cdo.bat @@ -0,0 +1,27 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +setlocal +set PWD=%cd% +cd %~dp0.. +set BASEDIR=%cd% +IF NOT DEFINED EMS_CONFIG_DIR set EMS_CONFIG_DIR=%BASEDIR%\config-files +IF NOT DEFINED PAASAGE_CONFIG_DIR set PAASAGE_CONFIG_DIR=%BASEDIR%\config-files + +:: Copy dependencies if missing +if exist pom.xml ( + if not exist %BASEDIR%\control-service\target\dependency cmd /C "cd control-service && mvn dependency:copy-dependencies" +) + +java -classpath %BASEDIR%/control-service/target/classes;%BASEDIR%/control-service/target/dependency/* gr.iccs.imu.ems.control.util.CpModelHelper %* +rem Usage: cp2cdo + +cd %PWD% +endlocal \ No newline at end of file diff --git a/ems-core/bin/cp2cdo.sh b/ems-core/bin/cp2cdo.sh new file mode 100644 index 0000000..4a9d1e1 --- /dev/null +++ b/ems-core/bin/cp2cdo.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +PREVWORKDIR=`pwd` +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) +cd ${BASEDIR} +if [[ -z $EMS_CONFIG_DIR ]]; then EMS_CONFIG_DIR=${BASEDIR}/config-files; export EMS_CONFIG_DIR; fi +if [[ -z $PAASAGE_CONFIG_DIR ]]; then PAASAGE_CONFIG_DIR=${BASEDIR}/config-files; export PAASAGE_CONFIG_DIR; fi + +# Copy dependencies if missing +if [[ -f ${BASEDIR}/control-service/pom.xml ]]; then + if [[ ! -d ${BASEDIR}/control-service/target/dependency ]]; then + cd ${BASEDIR}/control-service + mvn dependency:copy-dependencies + cd ${BASEDIR} + fi +fi + +java -classpath "control-service/target/classes;control-service/target/dependency/*" gr.iccs.imu.ems.control.util.CpModelHelper $* +# Usage: cp2cdo + +cd ${PREVWORKDIR} diff --git a/ems-core/bin/detect.sh b/ems-core/bin/detect.sh new file mode 100644 index 0000000..b996e90 --- /dev/null +++ b/ems-core/bin/detect.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +#Required utilities: grep,uniq,tr,cat,cut,uname. For commented commands, awk and wc. + +BUSYBOX_PREFIX="${args[0]}" + +#TMP_NUM_CPUS=$($BUSYBOX_PREFIX grep 'physical id' /proc/cpuinfo | $BUSYBOX_PREFIX sort | $BUSYBOX_PREFIX uniq | $BUSYBOX_PREFIX wc -l) +#TMP_NUM_CORES=$($BUSYBOX_PREFIX grep 'cpu cores' /proc/cpuinfo | $BUSYBOX_PREFIX sort | $BUSYBOX_PREFIX uniq | $BUSYBOX_PREFIX cut -d ' ' -f 3) +#TMP_NUM_PROCESSORS=$($BUSYBOX_PREFIX grep -c ^processor /proc/cpuinfo) +TMP_RAM_TOTAL_KB=$($BUSYBOX_PREFIX cat /proc/meminfo | $BUSYBOX_PREFIX grep MemTotal | $BUSYBOX_PREFIX tr -s ' ' | $BUSYBOX_PREFIX cut -d ' ' -f 2) +TMP_RAM_AVAILABLE_KB=$($BUSYBOX_PREFIX cat /proc/meminfo | $BUSYBOX_PREFIX grep MemAvailable | $BUSYBOX_PREFIX tr -s ' ' | $BUSYBOX_PREFIX cut -d ' ' -f 2) +TMP_RAM_FREE_KB=$($BUSYBOX_PREFIX cat /proc/meminfo | $BUSYBOX_PREFIX grep MemFree | $BUSYBOX_PREFIX tr -s ' ' | $BUSYBOX_PREFIX cut -d ' ' -f 2) +TMP_DISK_TOTAL_KB=$($BUSYBOX_PREFIX df -k | $BUSYBOX_PREFIX grep /$ | $BUSYBOX_PREFIX tr -s ' ' | $BUSYBOX_PREFIX cut -d ' ' -f 2) +TMP_DISK_FREE_KB=$($BUSYBOX_PREFIX df -k | $BUSYBOX_PREFIX grep /$ | $BUSYBOX_PREFIX tr -s ' ' | $BUSYBOX_PREFIX cut -d ' ' -f 4) +TMP_ARCHITECTURE=$($BUSYBOX_PREFIX uname -m) #x86_64 GNU/Linux indicates that you've a 64bit Linux kernel running. If you see i386/i486/i586/i686 it is a 32-bit architecture. armv7l, armv8 etc. signal a 32-bit arm version of the library while aarch64 indicates a 64-bit arm version of the library +TMP_KERNEL=$($BUSYBOX_PREFIX uname -s) +TMP_KERNEL_RELEASE=$($BUSYBOX_PREFIX uname -r) + +#NUM_CORES_ALT=$BUSYBOX_PREFIX grep ^cpu\\scores /proc/cpuinfo | $BUSYBOX_PREFIX uniq | $BUSYBOX_PREFIX awk '{print $4}' +#CAN_RUN_x64 = grep flags /proc/cpuinfo | grep " lm" | wc | tr -s ' ' | cut -d ' ' -f 2 #1 means that it can run x64, 0 that it can't, although that possibly also depends on the kernel installed + +TMP_NUM_CPUS=$(lscpu -p | grep -v '#' | cut -d ',' -f 3 | sort -u | wc -l) +TMP_NUM_CORES=$(lscpu -p | grep -v '#' | cut -d ',' -f 2 | sort -u | wc -l) +TMP_NUM_PROCESSORS=$(lscpu -p | grep -v '#' | cut -d ',' -f 1 | sort -u | wc -l) +TMP_RAM_USED_KB=$(echo $TMP_RAM_TOTAL_KB $TMP_RAM_FREE_KB | awk '{print $1 - $2}') +TMP_RAM_UTILIZATION=$(echo $TMP_RAM_USED_KB $TMP_RAM_TOTAL_KB | awk '{print 100 * $1 / $2}') +TMP_DISK_USED_KB=$(echo $TMP_DISK_TOTAL_KB $TMP_DISK_FREE_KB | awk '{print $1 - $2}') +TMP_DISK_UTILIZATION=$(echo $TMP_DISK_USED_KB $TMP_DISK_TOTAL_KB | awk '{print 100 * $1 / $2}') + + +echo CPU_SOCKETS=$TMP_NUM_CPUS +echo CPU_CORES=$TMP_NUM_CORES +echo CPU_PROCESSORS=$TMP_NUM_PROCESSORS +echo RAM_TOTAL_KB=$TMP_RAM_TOTAL_KB +echo RAM_AVAILABLE_KB=$TMP_RAM_AVAILABLE_KB +echo RAM_FREE_KB=$TMP_RAM_FREE_KB +echo RAM_USED_KB=$TMP_RAM_USED_KB +echo RAM_UTILIZATION=$TMP_RAM_UTILIZATION +echo DISK_TOTAL_KB=$TMP_DISK_TOTAL_KB +echo DISK_FREE_KB=$TMP_DISK_FREE_KB +echo DISK_USED_KB=$TMP_DISK_USED_KB +echo DISK_UTILIZATION=$TMP_DISK_UTILIZATION +echo OS_ARCHITECTURE=$TMP_ARCHITECTURE +echo OS_KERNEL=$TMP_KERNEL +echo OS_KERNEL_RELEASE=$TMP_KERNEL_RELEASE diff --git a/ems-core/bin/initialize-MELODIC-keystores.sh b/ems-core/bin/initialize-MELODIC-keystores.sh new file mode 100644 index 0000000..d8bbeab --- /dev/null +++ b/ems-core/bin/initialize-MELODIC-keystores.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +PREVWORKDIR=`pwd` +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +cd ${BASEDIR} + +if [[ -z $EMS_CONFIG_DIR ]]; then EMS_CONFIG_DIR=${BASEDIR}/config; export EMS_CONFIG_DIR; fi + +# Get IP addresses +echo Resolving Public IP addresses... +#PUBLIC_IP=`curl http://ifconfig.me 2> /dev/null` +#PUBLIC_IP=`curl http://www.icanhazip.com 2> /dev/null` +#PUBLIC_IP=`curl http://ipecho.net/plain 2> /dev/null` +#PUBLIC_IP=`curl http://bot.whatismyipaddress.com 2> /dev/null` +PUBLIC_IP=`curl https://diagnostic.opendns.com/myip 2> /dev/null` +#PUBLIC_IP=`curl http://checkip.amazonaws.com 2> /dev/null` + +# or get IP address with 'hostname' +if [[ "${PUBLIC_IP}" == "" ]]; then + PUBLIC_IP=`hostname --all-ip-addresses` + echo "PUBLIC_IP (hostname -I): $PUBLIC_IP" +fi + +# or set IP address manually +if [[ "${PUBLIC_IP}" == "" ]]; then + PUBLIC_IP=1.2.3.4 + echo "PUBLIC_IP (manually): $PUBLIC_IP" +fi + +# or use loopback +if [[ "${PUBLIC_IP}" == "" ]]; then + PUBLIC_IP=127.0.0.1 + echo "PUBLIC_IP (loopback): $PUBLIC_IP" +fi +PUBLIC_IP=`echo ${PUBLIC_IP} | sed 's/ *$//g'` +echo PUBLIC_IP=${PUBLIC_IP} + + +# Get cached IP address from previous run (if any) +CACHED_IP_FILE=${EMS_CONFIG_DIR}/MY_IP +touch ${CACHED_IP_FILE} +CACHED_IP=`cat ${CACHED_IP_FILE}` +#echo "Cached IP address=${CACHED_IP}" + +# Check if "Force update flag is set in command-line" (i.e. -U flag) +if [[ "$1" == "-U" ]]; then + CACHED_IP="----" +fi + +# Check if current and cached IP addresses match +if [[ "${PUBLIC_IP}" == "${CACHED_IP}" ]]; then + echo "Current and Cached IP addresses are identical: ${PUBLIC_IP}" + echo "Exit without changing keystores" + exit 0 +fi +# ...else store new IP address +echo ${PUBLIC_IP} > ${CACHED_IP_FILE} + + +# Prepare keystore base directory and truststore file +KEYSTORE_BASE_DIR=${EMS_CONFIG_DIR}/certs +TRUSTSTORE_DIR=${EMS_CONFIG_DIR}/common +TRUSTSTORE_FILE=${TRUSTSTORE_DIR}/melodic-truststore.p12 + +mkdir -p ${KEYSTORE_BASE_DIR} +mkdir -p ${TRUSTSTORE_DIR} +rm -f ${TRUSTSTORE_FILE} &> /dev/null + +# Keystore initialization settings +KEY_GEN_ALG=RSA +KEY_SIZE=2048 +START_DATE=-1d +VALIDITY=3650 +DN_FMT="CN=%s,OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR" +if [[ "${PUBLIC_IP}" != "" ]]; then + PUBLIC_IP_FOR_SAN=${PUBLIC_IP// /,ip:} + PUBLIC_IP_FOR_SAN="ip:${PUBLIC_IP_FOR_SAN}" +fi +if [[ "${EXTRA_IPS_FOR_SAN}" != "" ]]; then + EXTRA_IPS_FOR_SAN=",${EXTRA_IPS_FOR_SAN}" + EXTRA_IPS_FOR_SAN=`echo ${EXTRA_IPS_FOR_SAN} | sed -e 's/,/,ip:/g'` + EXTRA_IPS_FOR_SAN=`echo ${EXTRA_IPS_FOR_SAN} | sed -e 's/[ \t]//g'` +fi +EXT_SAN_FMT="SAN=dns:%s,dns:localhost,ip:127.0.0.1,${PUBLIC_IP_FOR_SAN}${EXTRA_IPS_FOR_SAN}" + +KEYSTORE_TYPE=PKCS12 +KEYSTORE_PASS=melodic + +# Definition of 'create_keystore_for' function for the: +# Creation of key pair and certificate for component +function create_keystore_for() { + local COMPONENT=$1 + local KEYSTORE_DIR=${KEYSTORE_BASE_DIR}/${COMPONENT} + local KEYSTORE_FILE=${KEYSTORE_DIR}/keystore.p12 + local CERT_FILE=${KEYSTORE_DIR}/${COMPONENT}.crt + local KEY_ALIAS=${COMPONENT} + local DN=`printf "${DN_FMT}" "${KEY_ALIAS}" ` + local EXT_SAN=`printf "${EXT_SAN_FMT}" "${KEY_ALIAS}" ` + + echo "$COMPONENT:" + mkdir -p ${KEYSTORE_DIR} + + echo " Generating key pair and certificate for ${COMPONENT}..." + rm -f ${KEYSTORE_FILE} &> /dev/null + keytool -genkey -keyalg ${KEY_GEN_ALG} -keysize ${KEY_SIZE} \ + -alias ${KEY_ALIAS} \ + -startdate ${START_DATE} -validity ${VALIDITY} \ + -dname "${DN}" -ext "${EXT_SAN}" \ + -keystore ${KEYSTORE_FILE} \ + -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} + + echo " Exporting certificate of ${COMPONENT}..." + rm -rf ${CERT_FILE} &> /dev/null + keytool -export \ + -alias ${KEY_ALIAS} \ + -file ${CERT_FILE} \ + -keystore ${KEYSTORE_FILE} \ + -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} + + echo " Importing ${COMPONENT} certificate to truststore..." + keytool -import -noprompt \ + -alias ${KEY_ALIAS} \ + -file ${CERT_FILE} \ + -keystore ${TRUSTSTORE_FILE} \ + -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} + + echo "" +} + +# Creation of key pairs, certificates of all components and population of common truststore +create_keystore_for "cdoserver" +create_keystore_for "mule" +create_keystore_for "adapter" +create_keystore_for "generator" +create_keystore_for "cpsolver" +create_keystore_for "camunda" +create_keystore_for "memcache" +create_keystore_for "ldap" +create_keystore_for "metasolver" +create_keystore_for "jwtserver" +create_keystore_for "authdb" +create_keystore_for "authserver" +create_keystore_for "ems" +create_keystore_for "gui-backend" +create_keystore_for "gui-frontend" +#create_keystore_for "cloudiator" + +echo Key stores, certificate and Melodic common truststores are ready. +cd $PREVWORKDIR diff --git a/ems-core/bin/initialize-keystores.bat b/ems-core/bin/initialize-keystores.bat new file mode 100644 index 0000000..69c3f77 --- /dev/null +++ b/ems-core/bin/initialize-keystores.bat @@ -0,0 +1,85 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +setlocal +set PWD=%cd% +cd %~dp0.. +set BASEDIR=%cd% +IF NOT DEFINED EMS_CONFIG_DIR set EMS_CONFIG_DIR=%BASEDIR%\config-files +IF NOT DEFINED PAASAGE_CONFIG_DIR set PAASAGE_CONFIG_DIR=%BASEDIR%\config-files + +:: Get IP addresses +set UTIL_FILE=util-4.0.0-SNAPSHOT-jar-with-dependencies.jar +set UTIL_PATH_0=util\target\%UTIL_FILE% +set UTIL_PATH_1=jars\util\%UTIL_FILE% +set UTIL_PATH_2=..\util\target\%UTIL_FILE% +set UTIL_PATH_3=.\%UTIL_FILE% +if exist %UTIL_PATH_0% ( + set UTIL_JAR=%UTIL_PATH_0% +) else ( + if exist %UTIL_PATH_1% ( + set UTIL_JAR=%UTIL_PATH_1% + ) else ( + if exist %UTIL_PATH_2% ( + set UTIL_JAR=%UTIL_PATH_2% + ) else ( + if exist %UTIL_PATH_3% ( + set UTIL_JAR=%UTIL_PATH_3% + ) else ( + echo ERROR: Couldn't find 'util-4.0.0-SNAPSHOT-jar-with-dependencies.jar' + echo ERROR: Skipping keystore initialization + goto the_end + ) + ) + ) +) +::echo UTIL_JAR location: %UTIL_JAR% + +echo Resolving Public and Default IP addresses... +for /f %%i in ('java -jar %UTIL_JAR% -nolog public') do set {PUBLIC_IP}=%%i +for /f %%i in ('java -jar %UTIL_JAR% -nolog default') do set {DEFAULT_IP}=%%i + +IF "%{PUBLIC_IP}%" == "null" set {PUBLIC_IP}=127.0.0.1 +IF "%{DEFAULT_IP}%" == "null" set {DEFAULT_IP}=127.0.0.1 + +echo PUBLIC_IP=%{PUBLIC_IP}% +echo DEFAULT_IP=%{DEFAULT_IP}% + +:: Keystore initialization settings +set KEY_GEN_ALG=RSA +set KEY_SIZE=2048 +set KEY_ALIAS=ems +set START_DATE=-1d +set VALIDITY=3650 +set DN=CN=ems,OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR +set EXT_SAN=SAN=dns:localhost,ip:127.0.0.1,ip:%{DEFAULT_IP}%,ip:%{PUBLIC_IP}% +set KEYSTORE=%EMS_CONFIG_DIR%\broker-keystore.p12 +set TRUSTSTORE=%EMS_CONFIG_DIR%\broker-truststore.p12 +set CERTIFICATE=%EMS_CONFIG_DIR%\broker.crt +set KEYSTORE_TYPE=PKCS12 +set KEYSTORE_PASS=melodic + +:: Keystores initialization +echo Generating key pair and certificate... +keytool -delete -alias %KEY_ALIAS% -keystore %KEYSTORE% -storetype %KEYSTORE_TYPE% -storepass %KEYSTORE_PASS% > nul 2>&1 +keytool -genkey -keyalg %KEY_GEN_ALG% -keysize %KEY_SIZE% -alias %KEY_ALIAS% -startdate %START_DATE% -validity %VALIDITY% -dname "%DN%" -ext "%EXT_SAN%" -keystore %KEYSTORE% -storetype %KEYSTORE_TYPE% -storepass %KEYSTORE_PASS% + +echo Exporting certificate to file... +del /Q %CERTIFICATE% > nul 2>&1 +keytool -export -alias %KEY_ALIAS% -file %CERTIFICATE% -keystore %KEYSTORE% -storetype %KEYSTORE_TYPE% -storepass %KEYSTORE_PASS% + +echo Importing certificate to trust store... +keytool -delete -alias %KEY_ALIAS% -keystore %TRUSTSTORE% -storetype %KEYSTORE_TYPE% -storepass %KEYSTORE_PASS% > nul 2>&1 +keytool -import -noprompt -file %CERTIFICATE% -alias %KEY_ALIAS% -keystore %TRUSTSTORE% -storetype %KEYSTORE_TYPE% -storepass %KEYSTORE_PASS% + +echo Key store, trust stores and certificate are ready. +:the_end +cd %PWD% +endlocal diff --git a/ems-core/bin/initialize-keystores.sh b/ems-core/bin/initialize-keystores.sh new file mode 100644 index 0000000..ba04518 --- /dev/null +++ b/ems-core/bin/initialize-keystores.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +PREVWORKDIR=`pwd` +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) +cd ${BASEDIR} +if [[ -z $EMS_CONFIG_DIR ]]; then EMS_CONFIG_DIR=$BASEDIR/config-files; export EMS_CONFIG_DIR; fi +if [[ -z $PAASAGE_CONFIG_DIR ]]; then PAASAGE_CONFIG_DIR=$BASEDIR/config-files; export PAASAGE_CONFIG_DIR; fi + +# Get IP addresses +UTIL_FILE=util-4.0.0-SNAPSHOT-jar-with-dependencies.jar +UTIL_PATH_0=util/target/${UTIL_FILE} +UTIL_PATH_1=jars/util/${UTIL_FILE} +UTIL_PATH_2=../util/target/${UTIL_FILE} +UTIL_PATH_3=./${UTIL_FILE} +if [ -f ${UTIL_PATH_0} ]; then + UTIL_JAR=${UTIL_PATH_0} +elif [ -f ${UTIL_PATH_1} ]; then + UTIL_JAR=${UTIL_PATH_1} +elif [ -f ${UTIL_PATH_2} ]; then + UTIL_JAR=${UTIL_PATH_2} +elif [ -f ${UTIL_PATH_3} ]; then + UTIL_JAR=${UTIL_PATH_3} +else + echo "ERROR: Couldn't find 'util-4.0.0-SNAPSHOT-jar-with-dependencies.jar'" + echo "ERROR: Skipping keystore initialization" + cd ${PREVWORKDIR} + exit 1 +fi +#echo UTIL_JAR location: ${UTIL_JAR} + +echo Resolving Public and Default IP addresses... +PUBLIC_IP=`java -jar ${UTIL_JAR} -nolog public` +DEFAULT_IP=`java -jar ${UTIL_JAR} -nolog default` + +if [[ "${PUBLIC_IP}" == "" || "${PUBLIC_IP}" == "null" ]]; then + PUBLIC_IP=127.0.0.1 +fi +if [[ "${DEFAULT_IP}" == "" || "${DEFAULT_IP}" == "null" ]]; then + DEFAULT_IP=127.0.0.1 +fi + +echo PUBLIC_IP=${PUBLIC_IP} +echo DEFAULT_IP=${DEFAULT_IP} + +# Keystore initialization settings +KEY_GEN_ALG=RSA +KEY_SIZE=2048 +KEY_ALIAS=ems +START_DATE=-1d +VALIDITY=3650 +DN="CN=ems,OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR" +EXT_SAN="SAN=dns:localhost,ip:127.0.0.1,ip:${DEFAULT_IP},ip:${PUBLIC_IP}" +KEYSTORE=${EMS_CONFIG_DIR}/broker-keystore.p12 +TRUSTSTORE=${EMS_CONFIG_DIR}/broker-truststore.p12 +CERTIFICATE=${EMS_CONFIG_DIR}/broker.crt +KEYSTORE_TYPE=PKCS12 +KEYSTORE_PASS=melodic + +# Keystores initialization +echo Generating key pair and certificate... +keytool -delete -alias ${KEY_ALIAS} -keystore ${KEYSTORE} -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} &> /dev/null +keytool -genkey -keyalg ${KEY_GEN_ALG} -keysize ${KEY_SIZE} -alias ${KEY_ALIAS} -startdate ${START_DATE} -validity ${VALIDITY} -dname "${DN}" -ext "${EXT_SAN}" -keystore ${KEYSTORE} -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} + +echo Exporting certificate to file... +rm -rf ${CERTIFICATE} &> /dev/null +keytool -export -alias ${KEY_ALIAS} -file ${CERTIFICATE} -keystore ${KEYSTORE} -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} + +echo Importing certificate to trust store... +keytool -delete -alias ${KEY_ALIAS} -keystore ${TRUSTSTORE} -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} &> /dev/null +keytool -import -noprompt -file ${CERTIFICATE} -alias ${KEY_ALIAS} -keystore ${TRUSTSTORE} -storetype ${KEYSTORE_TYPE} -storepass ${KEYSTORE_PASS} + +echo Key store, trust stores and certificate are ready. +cd $PREVWORKDIR diff --git a/ems-core/bin/jwtutil.bat b/ems-core/bin/jwtutil.bat new file mode 100644 index 0000000..75e3dab --- /dev/null +++ b/ems-core/bin/jwtutil.bat @@ -0,0 +1,33 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +setlocal +set PWD=%~dp0 +cd %PWD%.. +set BASEDIR=%cd% +IF NOT DEFINED EMS_CONFIG_DIR set EMS_CONFIG_DIR=%BASEDIR%\config-files +IF NOT DEFINED PAASAGE_CONFIG_DIR set PAASAGE_CONFIG_DIR=%BASEDIR%\config-files +IF NOT DEFINED JARS_DIR set JARS_DIR=%BASEDIR%\control-service\target + +if NOT DEFINED EMS_SECRETS_FILE set EMS_SECRETS_FILE=%EMS_CONFIG_DIR%\secrets.properties +if NOT DEFINED EMS_CONFIG_LOCATION set EMS_CONFIG_LOCATION=optional:file:%EMS_CONFIG_DIR%\ems-server.yml,optional:file:%EMS_CONFIG_DIR%\ems-server.properties,optional:file:%EMS_CONFIG_DIR%\ems.yml,optional:file:%EMS_CONFIG_DIR%\ems.properties,optional:file:%EMS_SECRETS_FILE% + +:: Read JASYPT password (decrypts encrypted configuration settings) +::set JASYPT_PASSWORD=password +if "%JASYPT_PASSWORD%"=="" ( + set /p JASYPT_PASSWORD="Configuration Password: " +) + +java -Djasypt.encryptor.password=%JASYPT_PASSWORD% -cp %JARS_DIR%\control-service.jar -Dloader.main=jwt.util.gr.iccs.imu.ems.control.JwtTokenUtil -Dlogging.level.ROOT=WARN -Dlogging.level.gr.iccs.imu.ems.util=ERROR "-Dspring.config.location=%EMS_CONFIG_LOCATION%" org.springframework.boot.loader.PropertiesLauncher %* +set exitcode=%ERRORLEVEL% + +cd %PWD% +endlocal && SET exitcode=%exitcode% +exit /B %exitcode% \ No newline at end of file diff --git a/ems-core/bin/jwtutil.sh b/ems-core/bin/jwtutil.sh new file mode 100644 index 0000000..26df3f0 --- /dev/null +++ b/ems-core/bin/jwtutil.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# Change directory to EMS home +PREVWORKDIR=`pwd` +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) +cd ${BASEDIR} +if [[ -z $EMS_CONFIG_DIR ]]; then EMS_CONFIG_DIR=$BASEDIR/config-files; export EMS_CONFIG_DIR; fi +if [[ -z $PAASAGE_CONFIG_DIR ]]; then PAASAGE_CONFIG_DIR=$BASEDIR/config-files; export PAASAGE_CONFIG_DIR; fi +if [[ -z $JARS_DIR ]]; then JARS_DIR=$BASEDIR/control-service/target; export JARS_DIR; fi + +if [[ -z EMS_SECRETS_FILE ]]; then EMS_SECRETS_FILE=$EMS_CONFIG_DIR/secrets.properties; export EMS_SECRETS_FILE; fi +if [[ -z EMS_CONFIG_LOCATION ]]; then EMS_CONFIG_LOCATION=optional:file:$EMS_CONFIG_DIR/ems-server.yml,optional:file:$EMS_CONFIG_DIR/ems-server.properties,optional:file:$EMS_CONFIG_DIR/ems.yml,optional:file:$EMS_CONFIG_DIR/ems.properties,optional:file:$EMS_SECRETS_FILE; export EMS_CONFIG_LOCATION; fi + +# Read JASYPT password (decrypts encrypted configuration settings) +#JASYPT_PASSWORD=password +if [[ -z "$JASYPT_PASSWORD" ]]; then + printf "Configuration Password: " + read -s JASYPT_PASSWORD +fi + +java -Djasypt.encryptor.password=$JASYPT_PASSWORD -cp ${JARS_DIR}/control-service.jar -Dloader.main=gr.iccs.imu.ems.control.util.jwt.JwtTokenUtil -Dlogging.level.ROOT=WARN -Dlogging.level.gr.iccs.imu.ems.util=ERROR "-Dspring.config.location=$EMS_CONFIG_LOCATION" org.springframework.boot.loader.PropertiesLauncher $* +exitcode=$? + +cd $PREVWORKDIR +exit $exitcode \ No newline at end of file diff --git a/ems-core/bin/run.bat b/ems-core/bin/run.bat new file mode 100644 index 0000000..fa74e74 --- /dev/null +++ b/ems-core/bin/run.bat @@ -0,0 +1,97 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +setlocal +set PWD=%~dp0 +cd %PWD%.. +set BASEDIR=%cd% +IF NOT DEFINED EMS_CONFIG_DIR set EMS_CONFIG_DIR=%BASEDIR%\config-files +IF NOT DEFINED PAASAGE_CONFIG_DIR set PAASAGE_CONFIG_DIR=%BASEDIR%\config-files +IF NOT DEFINED JARS_DIR set JARS_DIR=%BASEDIR%\control-service\target +IF NOT DEFINED LOGS_DIR set LOGS_DIR=%BASEDIR%\logs +IF NOT DEFINED PUBLIC_DIR set PUBLIC_DIR=%BASEDIR%\public_resources + +:: Read JASYPT password (decrypts encrypted configuration settings) +::set JASYPT_PASSWORD=password +if "%JASYPT_PASSWORD%"=="" ( + set /p JASYPT_PASSWORD="Configuration Password: " +) +:: Use this online service to encrypt/decrypt passwords: +:: https://www.devglan.com/online-tools/jasypt-online-encryption-decryption + +:: Check EMS configuration +if "%EMS_SECRETS_FILE%"=="" ( + set EMS_SECRETS_FILE=%EMS_CONFIG_DIR%\secrets.properties +) +if "%EMS_CONFIG_LOCATION%"=="" ( + set EMS_CONFIG_LOCATION=classpath:rule-templates.yml,optional:file:%EMS_CONFIG_DIR%\ems-server.yml,optional:file:%EMS_CONFIG_DIR%\ems-server.properties,optional:file:%EMS_CONFIG_DIR%\ems.yml,optional:file:%EMS_CONFIG_DIR%\ems.properties,optional:file:%EMS_SECRETS_FILE% +) + +:: Check logger configuration +if "%LOG_CONFIG_FILE%"=="" ( + set LOG_CONFIG_FILE=%EMS_CONFIG_DIR%\logback-conf\logback-spring.xml +) +echo Using logback config.: %LOG_CONFIG_FILE% +if "%LOG_FILE%"=="" ( + set LOG_FILE=%LOGS_DIR%\ems.log +) + +:: Set shell encoding to UTF-8 (in order to display banner correctly) +chcp 65001 + +:: Run EMS server +rem Uncomment next line to set JAVA runtime options +rem set JAVA_OPTS=-Djavax.net.debug=all + +set JAVA_ADD_OPENS=--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.regex=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED + +java -version +chcp +echo EMS_CONFIG_DIR=%EMS_CONFIG_DIR% +echo EMS_CONFIG_LOCATION=%EMS_CONFIG_LOCATION% +echo IP address: +ipconfig | findstr "/C:IPv4 Address" +echo Starting EMS server... +IF NOT DEFINED RESTART_EXIT_CODE set RESTART_EXIT_CODE=99 +:_restart_ems + +rem Check if fat-jar exists +if exist "%JARS_DIR%\control-service.jar" ( + set "CP=-cp %JARS_DIR%\control-service.jar" + set "ESPER_PATH=%JARS_DIR%\esper-7.1.0.jar," +) + +rem Use when Esper is packaged in control-service.jar +rem java %EMS_DEBUG_OPTS% %JAVA_OPTS% %JAVA_ADD_OPENS% -Djasypt.encryptor.password=%JASYPT_PASSWORD% -Djava.security.egd=file:/dev/urandom -jar %JARS_DIR%\control-service.jar -nolog "--spring.config.location=%EMS_CONFIG_LOCATION%" "--logging.config=file:%LOG_CONFIG_FILE%" + +rem Use when Esper is NOT packaged in control-service.jar +java %EMS_DEBUG_OPTS% %JAVA_OPTS% %JAVA_ADD_OPENS% ^ + -Djasypt.encryptor.password=%JASYPT_PASSWORD% ^ + -Djava.security.egd=file:/dev/urandom ^ + -Dscan.packages=%SCAN_PACKAGES% ^ + %CP% ^ + "-Dloader.path=%ESPER_PATH%%EXTRA_LOADER_PATHS%" ^ + org.springframework.boot.loader.PropertiesLauncher ^ + -nolog ^ + "--spring.config.location=%EMS_CONFIG_LOCATION%" ^ + "--logging.config=file:%LOG_CONFIG_FILE%" ^ + %* + +if errorlevel %RESTART_EXIT_CODE% ( + echo Restarting EMS server... + goto :_restart_ems +) +echo EMS server exited + +rem e.g. --spring.config.location=%EMS_CONFIG_DIR%\ +rem e.g. --spring.config.name=application.properties + +cd %PWD% +endlocal \ No newline at end of file diff --git a/ems-core/bin/run.sh b/ems-core/bin/run.sh new file mode 100644 index 0000000..ef22880 --- /dev/null +++ b/ems-core/bin/run.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# Change directory to EMS home +PREVWORKDIR=`pwd` +BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) +cd ${BASEDIR} +if [[ -z $EMS_CONFIG_DIR ]]; then EMS_CONFIG_DIR=$BASEDIR/config-files; export EMS_CONFIG_DIR; fi +if [[ -z $PAASAGE_CONFIG_DIR ]]; then PAASAGE_CONFIG_DIR=$BASEDIR/config-files; export PAASAGE_CONFIG_DIR; fi +if [[ -z $JARS_DIR ]]; then JARS_DIR=$BASEDIR/control-service/target; export JARS_DIR; fi +if [[ -z $LOGS_DIR ]]; then LOGS_DIR=$BASEDIR/logs; export LOGS_DIR; fi +if [[ -z $PUBLIC_DIR ]]; then PUBLIC_DIR=$BASEDIR/public_resources; export PUBLIC_DIR; fi + +# Read JASYPT password (decrypts encrypted configuration settings) +#JASYPT_PASSWORD=password +if [[ -z "$JASYPT_PASSWORD" ]]; then + printf "Configuration Password: " + read -s JASYPT_PASSWORD +fi +# Use this online service to encrypt/decrypt passwords: +# https://www.devglan.com/online-tools/jasypt-online-encryption-decryption + +export JASYPT_PASSWORD + +# Check EMS configuration +if [[ -z "$EMS_SECRETS_FILE" ]]; then + EMS_SECRETS_FILE=$EMS_CONFIG_DIR/secrets.properties +fi +if [[ -z "$EMS_CONFIG_LOCATION" ]]; then + EMS_CONFIG_LOCATION=classpath:rule-templates.yml,optional:file:$EMS_CONFIG_DIR/ems-server.yml,optional:file:$EMS_CONFIG_DIR/ems-server.properties,optional:file:$EMS_CONFIG_DIR/ems.yml,optional:file:$EMS_CONFIG_DIR/ems.properties,optional:file:$EMS_SECRETS_FILE +fi + +# Check logger configuration +if [[ -z "$LOG_CONFIG_FILE" ]]; then + LOG_CONFIG_FILE=$EMS_CONFIG_DIR/logback-conf/logback-spring.xml +fi +if [[ -z "$LOG_FILE" ]]; then + LOG_FILE=$LOGS_DIR/ems.log + export LOG_FILE +fi + +# Set shell encoding to UTF-8 (in order to display banner correctly) +export LANG=C.UTF-8 + +# Setup TERM & INT signal handler +trap 'echo "Signaling EMS to exit"; kill -TERM "${emsPid}"; wait "${emsPid}"; ' SIGTERM SIGINT + +# Run EMS server +# Uncomment next line to set JAVA runtime options +#JAVA_OPTS=-Djavax.net.debug=all +#JAVA_OPTS=-agentlib:native-image-agent=config-output-dir=/mnt/ems/control-service/src/main/resources/META-INF/native-image +#JAVA_OPTS=-agentlib:native-image-agent=config-merge-dir=/mnt/ems/control-service/src/main/resources/META-INF/native-image +#export JAVA_OPTS + +JAVA_ADD_OPENS="--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.regex=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED" + +# Check if fat-jar exists +if [[ -f "${JARS_DIR}/control-service.jar" ]]; then + CP="-cp ${JARS_DIR}/control-service.jar" + ESPER_PATH="${JARS_DIR}/esper-7.1.0.jar," +fi + +java -version +echo "LANG=$LANG" +#locale +echo "EMS_CONFIG_DIR=${EMS_CONFIG_DIR}" +echo "EMS_CONFIG_LOCATION=${EMS_CONFIG_LOCATION}" +echo "IP address=`hostname -I`" +echo "Starting EMS server..." +if [[ -z $RESTART_EXIT_CODE ]]; then RESTART_EXIT_CODE=99; export RESTART_EXIT_CODE; fi +retCode=$RESTART_EXIT_CODE +while :; do + # Use when Esper is packaged in control-service.jar + # java $EMS_DEBUG_OPTS $JAVA_OPTS $JAVA_ADD_OPENS -Djasypt.encryptor.password=$JASYPT_PASSWORD -Djava.security.egd=file:/dev/urandom -jar $JARS_DIR/control-service/target/control-service.jar "--spring.config.location=${EMS_CONFIG_LOCATION}" "--logging.config=file:$LOG_CONFIG_FILE" + + # Use when Esper is NOT packaged in control-service.jar + java $EMS_DEBUG_OPTS $JAVA_OPTS $JAVA_ADD_OPENS \ + -Djasypt.encryptor.password=$JASYPT_PASSWORD \ + -Djava.security.egd=file:/dev/urandom \ + -Dscan.packages=${SCAN_PACKAGES} \ + ${CP} \ + -Dloader.path=${ESPER_PATH}${EXTRA_LOADER_PATHS} \ + org.springframework.boot.loader.PropertiesLauncher \ + "--spring.config.location=${EMS_CONFIG_LOCATION}" \ + "--logging.config=file:$LOG_CONFIG_FILE" \ + $* & + emsPid=$! + echo "EMS Pid: $emsPid" + wait $emsPid + + retCode=$? + if [[ $retCode -eq $RESTART_EXIT_CODE ]]; then echo "Restarting EMS server..."; else break; fi +done +echo "EMS server exited" + +# Extra parameters +# e.g. --spring.config.location=$EMS_CONFIG_DIR +# e.g. --spring.config.name=application.properties + +cd $PREVWORKDIR +exit $retCode \ No newline at end of file diff --git a/ems-core/bin/sysmon.sh b/ems-core/bin/sysmon.sh new file mode 100644 index 0000000..4660ecf --- /dev/null +++ b/ems-core/bin/sysmon.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# Current Time / Start Time / Uptime +curr_dt=`date '+%Y-%m-%d %H:%M:%S'` +up_dt=`uptime -s` +curr_dt_sec=`date -d "$curr_dt" +%s` +up_dt_sec=`date -d "$up_dt" +%s` +uptime_sec=$(( curr_dt_sec - up_dt_sec )) +echo CurrDateTime: $curr_dt_sec +echo UpDateTime: $up_dt_sec +echo Uptime: $uptime_sec + +# Report CPU usage (%) +echo CPU: `top -b -n1 | grep "Cpu(s)" | awk '{print $2 + $4}'` + +# Report Memory usage (%) +FREE_DATA=`free -m | grep Mem` +CURRENT=`echo $FREE_DATA | cut -f3 -d' '` +TOTAL=`echo $FREE_DATA | cut -f2 -d' '` +echo RAM: $(echo "$CURRENT $TOTAL" | awk '{print 100 * $1 / $2}' ) + +# Report Disk usage (%) -- '/' partition only +#echo DISK: `df -lh | awk '{if ($6 == "/") { print $5 }}' | head -1 | cut -d'%' -f1` +echo DISK: `df -lh | awk '{if ($6 == "/") { print 100 * $3 / $2 }}'` + +# Report Network RX/TX usage (B/s) +ARR=($(ls -1 /sys/class/net/ | grep eth)) + +function measure_ifs() { + local SUMRX=0 + local SUMTX=0 + for IF in "${ARR[@]}"; do + let SUMRX=$SUMRX+`cat /sys/class/net/${IF}/statistics/rx_bytes` + let SUMTX=$SUMTX+`cat /sys/class/net/${IF}/statistics/tx_bytes` + done + echo $SUMRX $SUMTX +} + +START=($(measure_ifs)) +sleep 1 +END=($(measure_ifs)) + +RX=$(( END[0] - START[0] )) +TX=$(( END[1] - START[1] )) +echo RX: $RX +echo TX: $TX diff --git a/ems-core/bin/update-credentials.sh b/ems-core/bin/update-credentials.sh new file mode 100644 index 0000000..d49a2a5 --- /dev/null +++ b/ems-core/bin/update-credentials.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +echo "Updating broker client credentials..." + +# Generate new username/password pair +NEWUSERNAME=user-`< /dev/urandom tr -cd "[:alnum:]" | head -c 32` +NEWPASSWORD=`< /dev/urandom tr -cd "[:alnum:]" | head -c 32` +NEWCREDENTIALS="$NEWUSERNAME\/$NEWPASSWORD" +echo "-New username: $NEWUSERNAME" +echo "-New password: $NEWPASSWORD" +echo "-BCEP credentials: $NEWCREDENTIALS" + +# Update all files passed as arguments +for file in "$@" +do + printf " * Updating file %s..." $file + # Updating the brokerclient style properties... + sed -i "s/^\s*brokerclient.broker-username\s*[=:].*/brokerclient.broker-username=$NEWUSERNAME/" $file + sed -i "s/^\s*brokerclient.broker-password\s*[=:].*/brokerclient.broker-password=$NEWPASSWORD/" $file + # Updating the brokercep style properties... + sed -i "s/^\s*brokercep.additional-broker-credentials\s*[=:].*/brokercep.additional-broker-credentials=$NEWCREDENTIALS/" $file + echo "ok" +done + +echo "done" diff --git a/ems-core/broker-cep/client.bat b/ems-core/broker-cep/client.bat new file mode 100644 index 0000000..4b9ecf8 --- /dev/null +++ b/ems-core/broker-cep/client.bat @@ -0,0 +1,25 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +if not exist target\dependency cmd /C "mvn dependency:copy-dependencies" + +setlocal +set JAVA_OPTS= -Djavax.net.ssl.keyStore=..\config-files\broker-keystore.p12 ^ + -Djavax.net.ssl.keyStoreType=pkcs12 ^ + -Djavax.net.ssl.keyStorePassword=melodic ^ + -Djavax.net.ssl.trustStore=..\config-files\broker-truststore.p12 ^ + -Djavax.net.ssl.trustStorePassword=melodic ^ + -Djavax.net.ssl.trustStoreType=pkcs12 +rem -Djavax.net.debug=all +rem -Djavax.net.debug=ssl,handshake,record + +java %JAVA_OPTS% -classpath "target\classes;target\dependency\*" gr.iccs.imu.ems.brokercep.broker.BrokerClient %* + +endlocal diff --git a/ems-core/broker-cep/pom.xml b/ems-core/broker-cep/pom.xml new file mode 100644 index 0000000..294b2c1 --- /dev/null +++ b/ems-core/broker-cep/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + broker-cep + EMS - Broker+CEP Service + + + + + gr.iccs.imu.ems + util + ${project.version} + + + + + + org.springframework + spring-jms + + + org.apache.activemq + activemq-client + ${activemq.version} + + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.apache.activemq + activemq-broker + ${activemq.version} + + + org.apache.activemq + activemq-kahadb-store + ${activemq.version} + + + org.apache.activemq + activemq-jaas + ${activemq.version} + + + org.apache.activemq + activemq-stomp + ${activemq.version} + + + org.apache.activemq + activemq-pool + ${activemq.version} + + + + + com.espertech + esper + ${esper.version} + + + + + org.mariuszgromada.math + MathParser.org-mXparser + ${mathparser.version} + + + + + org.projectlombok + lombok + provided + + + + + org.apache.commons + commons-lang3 + + + + + org.apache.commons + commons-csv + 1.7 + + + + diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepConsumer.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepConsumer.java new file mode 100644 index 0000000..1ed5172 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepConsumer.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep; + +import gr.iccs.imu.ems.brokercep.broker.BrokerConfig; +import gr.iccs.imu.ems.brokercep.cep.CepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.BrokerService; +import org.apache.activemq.command.ActiveMQDestination; +import org.apache.activemq.command.ActiveMQObjectMessage; +import org.apache.activemq.command.ActiveMQTextMessage; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import javax.jms.*; +import java.time.Instant; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BrokerCepConsumer implements MessageListener, InitializingBean, ApplicationListener { + private final static AtomicLong eventCounter = new AtomicLong(0); + private final static AtomicLong textEventCounter = new AtomicLong(0); + private final static AtomicLong objectEventCounter = new AtomicLong(0); + private final static AtomicLong otherEventCounter = new AtomicLong(0); + private final static AtomicLong eventFailuresCounter = new AtomicLong(0); + + private final BrokerCepProperties properties; + private final BrokerConfig brokerConfig; + private final BrokerService brokerService; // Added in order to ensure that BrokerService will be instantiated first + private final CepService cepService; + + private Connection connection; + private Session session; + private final Map addedDestinations = new HashMap<>(); + + private final TaskScheduler scheduler; + private boolean shuttingDown; + + private final EventCache eventCache; + + @Override + public void afterPropertiesSet() { + initialize(); + } + + public synchronized void initialize() { + log.debug("BrokerCepConsumer.initialize(): Initializing Broker-CEP consumer instance..."); + try { + // close previous session and connection + closeConnection(); + + // clear added destinations list + addedDestinations.clear(); + + // If an alternative Broker URL is provided for consumer, it will be used + ConnectionFactory connectionFactory = brokerConfig.getConnectionFactoryForConsumer(); + + // Initialize connection + connection = (brokerConfig.getBrokerLocalAdminUsername() != null) + ? connectionFactory.createConnection(brokerConfig.getBrokerLocalAdminUsername(), brokerConfig.getBrokerLocalAdminPassword()) + : connectionFactory.createConnection(); + connection.setExceptionListener(e -> { + if (!shuttingDown) { + log.warn("BrokerCepConsumer: Connection exception listener: Exception caught: ", e); + scheduler.schedule(this::initialize, Instant.now()); + } + }); + connection.start(); + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + log.debug("BrokerCepConsumer.initialize(): Initializing Broker-CEP consumer instance... done"); + } catch (Exception ex) { + log.error("BrokerCepConsumer.initialize(): EXCEPTION: ", ex); + } + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + log.info("BrokerCepConsumer is shutting down"); + shuttingDown = true; + } + + private void closeConnection() { + // close previous session and connection + try { + if (session != null) { + session.close(); + log.debug("BrokerCepConsumer.closeConnection(): Closed pre-existing sessions"); + } + } catch (Exception e) { + log.warn("BrokerCepConsumer.closeConnection(): Exception while closing old session: ", e); + } + try { + if (connection != null) { + connection.close(); + log.debug("BrokerCepConsumer.closeConnection(): Closed pre-existing connection"); + } + } catch (Exception e) { + log.warn("BrokerCepConsumer.closeConnection(): Exception while closing old connection: ", e); + } + session = null; + connection = null; + } + + public synchronized void addQueue(String queueName) { + log.debug("BrokerCepConsumer.addQueue(): Adding queue: {}", queueName); + if (addedDestinations.containsKey(queueName)) { + log.debug("BrokerCepConsumer.addQueue(): Queue already added: {}", queueName); + return; + } + try { + Queue queue = session.createQueue(queueName); + MessageConsumer consumer = session.createConsumer(queue); + consumer.setMessageListener(this); + addedDestinations.put(queueName, consumer); + log.debug("BrokerCepConsumer.addQueue(): Added queue: {}", queueName); + } catch (Exception ex) { + log.error("BrokerCepConsumer.addQueue(): EXCEPTION: ", ex); + } + } + + public synchronized void addTopic(String topicName) { + log.debug("BrokerCepConsumer.addTopic(): Adding topic: {}", topicName); + if (addedDestinations.containsKey(topicName)) { + log.debug("BrokerCepConsumer.addTopic(): Topic already added: {}", topicName); + return; + } + try { + Topic topic = session.createTopic(topicName); + MessageConsumer consumer = session.createConsumer(topic); + consumer.setMessageListener(this); + addedDestinations.put(topicName, consumer); + log.debug("BrokerCepConsumer.addTopic(): Added topic: {}", topicName); + } catch (Exception ex) { + log.error("BrokerCepConsumer.addTopic(): EXCEPTION: ", ex); + } + } + + public synchronized void removeConsumerOf(String name) { + log.debug("BrokerCepConsumer.removeConsumerOf(): Removing topic or queue: {}", name); + if (!addedDestinations.containsKey(name)) { + log.debug("BrokerCepConsumer.removeConsumerOf(): Topic/Queue not exists: {}", name); + return; + } + try { + MessageConsumer consumer = addedDestinations.remove(name); + if (consumer!=null) consumer.close(); + log.debug("BrokerCepConsumer.removeConsumerOf(): Removed topic: {}", name); + } catch (Exception ex) { + log.error("BrokerCepConsumer.removeConsumerOf(): EXCEPTION: ", ex); + } + } + + public boolean containsDestination(String name) { + return addedDestinations.containsKey(name); + } + + @Override + public void onMessage(Message message) { + // Log message + logMessage(message); + + // Record message + if (brokerConfig.getEventRecorder()!=null) + brokerConfig.getEventRecorder().recordRegisteredEvent(message); + + // Handle message + try { + log.trace("BrokerCepConsumer.onMessage(): {}", message); + if (message instanceof ActiveMQObjectMessage mesg) { + ActiveMQDestination messageDestination = mesg.getDestination(); + log.debug("BrokerCepConsumer.onMessage(): Message received: source={}, payload={}", + messageDestination.getPhysicalName(), mesg.getObject()); + + // Send message to Esper + if (mesg.getObject() instanceof Map) { + //cepService.handleEvent(StrUtil.castToMapStringObject(mesg.getObject()), messageDestination.getPhysicalName()); + EventMap eventMap = new EventMap(StrUtil.castToMapStringObject(mesg.getObject())); + copyEventProperties(message, eventMap); + cepService.handleEvent(eventMap, messageDestination.getPhysicalName()); + eventCache.cacheEvent(eventMap, messageDestination.getPhysicalName()); + } else { + if (mesg.getObject()!=null) { + cepService.handleEvent(mesg.getObject()); + eventCache.cacheEvent(mesg.getObject(), null, messageDestination.getPhysicalName()); + } + } + objectEventCounter.incrementAndGet(); + } else if (message instanceof ActiveMQTextMessage mesg) { + ActiveMQDestination messageDestination = mesg.getDestination(); + log.debug("BrokerCepConsumer.onMessage(): Message received: source={}, payload={}, mime={}", + messageDestination.getPhysicalName(), mesg.getText(), mesg.getJMSXMimeType()); + + // Send message to Esper + //cepService.handleEvent(mesg.getText(), messageDestination.getPhysicalName()); + EventMap eventMap = new com.google.gson.Gson().fromJson(mesg.getText(), EventMap.class); + copyEventProperties(message, eventMap); + log.trace("BrokerCepConsumer.onMessage(): event-map={}", eventMap); + cepService.handleEvent(eventMap, messageDestination.getPhysicalName()); + eventCache.cacheEvent(eventMap, messageDestination.getPhysicalName()); + textEventCounter.incrementAndGet(); + } else { + otherEventCounter.incrementAndGet(); + log.warn("BrokerCepConsumer.onMessage(): Message ignored: type={}", message.getClass().getName()); + } + eventCounter.incrementAndGet(); + } catch (Exception ex) { + log.error("BrokerCepConsumer.onMessage(): EXCEPTION: ", ex); + eventFailuresCounter.incrementAndGet(); + } + } + + private void logMessage(Message message) { + boolean logBrokerMessages = properties.isLogBrokerMessages(); + boolean logBrokerMessagesFull = properties.isLogBrokerMessagesFull(); + if (!logBrokerMessages) return; + + try { + // Check if message passed is null + if (message==null) { + log.warn("\n==========| **NULL** MESSAGE RECEIVED"); + return; + } + + // Extract important message data (id, destination, metric-value) + String jmsMesgId = message.getJMSMessageID(); + Destination jmsDest = message.getJMSDestination(); + String mesgStr = message.toString(); + String metricValue = StringUtils.substringBetween(mesgStr, "metricValue", ","); + if (metricValue==null) metricValue = StringUtils.substringBetween(mesgStr, "metricValue", "}"); + if (metricValue!=null) metricValue = metricValue.replace("\"", "").replace(":", "").trim(); + else metricValue = logBrokerMessagesFull ? "---See next---" : "---Not found---"; + + // Log message data + if (logBrokerMessagesFull) + log.info("\n==========| RECEIVED A MESSAGE: metricValue={}, dest={}, id={}\n{}", metricValue, jmsDest, jmsMesgId, message); + else + log.info("\n==========| RECEIVED A MESSAGE: metricValue={}, dest={}, id={}", metricValue, jmsDest, jmsMesgId); + + } catch (Exception e) { + // Log error + if (logBrokerMessagesFull) + log.warn("\n==========| RECEIVED A MESSAGE: FAILED TO PARSE. SEE NEXT FOR STACKTRACE\n{}\n\nSTACKTRACE:\n", message, e); + else + log.warn("\n==========| RECEIVED A MESSAGE: FAILED TO PARSE. SEE NEXT FOR STACKTRACE\n\nSTACKTRACE:\n", e); + } + } + + private EventMap copyEventProperties(Message message, EventMap eventMap) throws JMSException { + log.debug("BrokerCepConsumer.copyEventProperties(): BEGIN: message={}, event={}", message, eventMap); + + // Copy message properties to event map + Collections.list((Enumeration) message.getPropertyNames()).forEach(s -> { + String n = s.toString(); + log.trace("BrokerCepConsumer.copyEventProperties(): Copying property: message={}, event={}, property={}", message, eventMap, n); + try { + String v = message.getStringProperty(n); + eventMap.setEventProperty(n, v); + log.debug("BrokerCepConsumer.copyEventProperties(): Copied property: message={}, event={}, property={}, value={}", message, eventMap, n, v); + } catch (Exception e) { + log.debug("BrokerCepConsumer.copyEventProperties(): EXCEPTION: while copying property: message={}, event={}, property={}, Exception: ", message, eventMap, n, e); + } + }); + return eventMap; + } + + public static long getEventCounter() { return eventCounter.get(); } + public static long getTextEventCounter() { return textEventCounter.get(); } + public static long getObjectEventCounter() { return objectEventCounter.get(); } + public static long getOtherEventCounter() { return otherEventCounter.get(); } + public static long getEventFailuresCounter() { return eventFailuresCounter.get(); } + public static synchronized void clearCounters() { + eventCounter.set(0L); + textEventCounter.set(0L); + objectEventCounter.set(0L); + otherEventCounter.set(0L); + eventFailuresCounter.set(0L); + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepService.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepService.java new file mode 100644 index 0000000..c41ae73 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepService.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep; + +import com.google.gson.Gson; +import gr.iccs.imu.ems.brokercep.broker.BrokerConfig; +import gr.iccs.imu.ems.brokercep.cep.CepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import gr.iccs.imu.ems.util.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.BrokerService; +import org.apache.activemq.broker.jmx.BrokerView; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.jms.*; +import javax.management.ObjectName; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Slf4j +@Service +@AllArgsConstructor(onConstructor = @__({@Autowired})) +public class BrokerCepService { + private BrokerCepProperties properties; + private BrokerConfig brokerConfig; + @Getter + private BrokerService brokerService; + private PasswordUtil passwordUtil; + + @Getter + private BrokerCepConsumer brokerCepBridge; + @Getter + private CepService cepService; + private EventCache eventCache; + + private Gson gson; + + public BrokerCepProperties getBrokerCepProperties() { + return properties; + } + + public synchronized void clearState() { + log.debug("BrokerCepService.clearState(): Clearing Broker-CEP state..."); + + // Clear CEP service state + cepService.clearStatements(); + cepService.clearEventTypes(); + cepService.clearConstants(); + cepService.clearFunctionDefinitions(); + + // Clear Broker service state + try { + BrokerView bv = brokerService.getAdminView(); + ObjectName[] queues = bv.getQueues(); + //ObjectName[] queueSubscribers = bv.getQueueSubscribers(); + ObjectName[] topics = bv.getTopics(); + //ObjectName[] topicSubscribers = bv.getTopicSubscribers(); + for (ObjectName q : queues) { + String name = q.getCanonicalName(); + bv.removeQueue(name); + log.debug("BrokerCepService.clearState(): Queue removed: {}", name); + } + for (ObjectName t : topics) { + String name = t.getCanonicalName(); + bv.removeTopic(name); + log.debug("BrokerCepService.clearState(): Topic removed: {}", name); + } + + log.debug("BrokerCepService.clearState(): Broker-CEP state cleared"); + } catch (Exception ex) { + log.error("BrokerCepService.clearState(): Failed to clear Broker state: ", ex); + } + + // Reset Broker-CEP Consumer connection and session + brokerCepBridge.initialize(); + log.debug("BrokerCepService.clearState(): Broker-CEP Consumer has been re-initialized"); + } + + public synchronized void addEventTypes(Set eventTypeNames, String[] eventPropertyNames, Class[] eventPropertyTypes) { + log.info("BrokerCepService.addEventTypes(): Adding event types: {}", eventTypeNames); + eventTypeNames.forEach(name -> addEventType(name, eventPropertyNames, eventPropertyTypes)); + log.debug("BrokerCepService.addEventTypes(): Adding event types: ok"); + } + + public synchronized void addEventTypes(Set eventTypeNames, Class eventType) { + log.info("BrokerCepService.addEventTypes(): Adding event types: {}", eventTypeNames); + eventTypeNames.forEach(name -> addEventType(name, eventType)); + log.debug("BrokerCepService.addEventTypes(): Adding event types: ok"); + } + + public synchronized void addEventType(String eventTypeName, String[] eventPropertyNames, Class[] eventPropertyTypes) { + // Add a new queue/topic in ActiveMQ (broker) named after 'eventTypeName' + //brokerCepBridge.addQueue(eventTypeName); + brokerCepBridge.addTopic(eventTypeName); + + // Register a new event type in Esper (cep engine) + cepService.addEventType(eventTypeName, eventPropertyNames, eventPropertyTypes); + log.debug("BrokerCepService.addEventType(): New event type registered: {}", eventTypeName); + } + + public synchronized void addEventType(String eventTypeName, Class eventType) { + // Add a new queue/topic in ActiveMQ (broker) named after 'eventTypeName' + //brokerCepBridge.addQueue(eventTypeName); + brokerCepBridge.addTopic(eventTypeName); + + // Register a new event type in Esper (cep engine) + cepService.addEventType(eventTypeName, eventType); + log.debug("BrokerCepService.addEventType(): New event type registered: {}", eventTypeName); + } + + public void setConstant(String constName, double constValue) { + log.debug("BrokerCepService.setConstant(): Add/Set constant: name={}, value={}", constName, constValue); + cepService.setConstant(constName, constValue); + } + + public void setConstants(Map constants) { + log.info("BrokerCepService.setConstants(): Add/Set constants: {}", constants); + cepService.setConstants(constants); + log.debug("BrokerCepService.setConstants(): Add/Set constants: ok"); + } + + public void addFunctionDefinitions(Set definitions) { + log.info("BrokerCepService.addFunctionDefinitions(): Adding function definitions: {}", definitions); + definitions.forEach(this::addFunctionDefinition); + log.debug("BrokerCepService.addFunctionDefinitions(): Adding function definitions: ok"); + } + + public void addFunctionDefinition(FunctionDefinition definition) { + log.info("BrokerCepService.addFunction(): New function definition registered: {}", definition); + cepService.addFunctionDefinition(definition); + } + + public boolean destinationExists(String destination) { + return brokerCepBridge.containsDestination(destination); + } + + public synchronized void publishEvent(String connectionString, String destinationName, Map eventMap) throws JMSException { + if (properties.isBypassLocalBroker() && _publishLocalEvent(connectionString, destinationName, new EventMap(eventMap))) + return; + _publishEvent(connectionString, destinationName, EventMap.toEventMap(eventMap), true); + } + + public synchronized void publishEvent(String connectionString, String username, String password, String destinationName, Map eventMap) throws JMSException { + if (properties.isBypassLocalBroker() && _publishLocalEvent(connectionString, destinationName, new EventMap(eventMap))) + return; + _publishEvent(connectionString, username, password, destinationName, new EventMap(eventMap), true); + } + + public synchronized void publishSerializable(String connectionString, String destinationName, Serializable event, boolean convertToJson) throws JMSException { + if (properties.isBypassLocalBroker() && _publishLocalEvent(connectionString, destinationName, event)) + return; + _publishEvent(connectionString, destinationName, event, convertToJson); + } + + public synchronized void publishSerializable(String connectionString, String username, String password, String destinationName, Serializable event, boolean convertToJson) throws JMSException { + if (properties.isBypassLocalBroker() && _publishLocalEvent(connectionString, destinationName, event)) + return; + _publishEvent(connectionString, username, password, destinationName, event, convertToJson); + } + + // When destination is the local broker then hand event to (local) CEP engine, bypassing local broker + private final static java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile("^([a-z]+://[a-zA-Z0-9_\\.\\-]+:[0-9]+)([/#\\?].*)?$"); + + private synchronized boolean _publishLocalEvent(String connectionString, String destinationName, Serializable event) throws JMSException { + java.util.regex.Matcher matcher = urlPattern.matcher(connectionString); + String connBrokerUrl = matcher.matches() ? matcher.group(1) : connectionString; + log.debug("BrokerCepService._publishLocalEvent(): Check if event is published to the local broker: local-broker-url={}, connection-broker-url={}, connection={}, destination={}, payload={}", + properties.getBrokerUrl(), connBrokerUrl, connectionString, destinationName, event); + if (!connBrokerUrl.equals(properties.getBrokerUrl())) return false; + + Class eventClass = event.getClass(); + log.debug("BrokerCepService._publishLocalEvent(): It is local event. Skipping publish through broker: connection={}, destination={}, payload-class={}, payload={}", + connectionString, destinationName, eventClass.getName(), event); + if (String.class.isAssignableFrom(eventClass)) { + log.debug("BrokerCepService._publishLocalEvent(): String event..."); + cepService.handleEvent((String) event, destinationName); + } else if (Map.class.isAssignableFrom(eventClass)) { + log.debug("BrokerCepService._publishLocalEvent(): Map event..."); + cepService.handleEvent(StrUtil.castToMapStringObject(event), destinationName); + } else { + log.debug("BrokerCepService._publishLocalEvent(): Object event..."); + cepService.handleEvent(event); + } + return true; + } + + private synchronized void _publishEvent(String connectionString, String destinationName, Serializable event, boolean convertToJson) throws JMSException { + // Get username/password for local broker service + String username = null; + String password = null; + if (_isLocalBrokerUrl(connectionString)) { + username = brokerConfig.getBrokerLocalAdminUsername(); + password = brokerConfig.getBrokerLocalAdminPassword(); + log.debug("BrokerCepService._publishEvent(): Using LOCAL BROKER credentials: {} / {}", + username, passwordUtil.encodePassword(password)); + } + _publishEvent(connectionString, username, password, destinationName, event, convertToJson); + } + + private synchronized void _publishEvent(String connectionString, String username, String password, String destinationName, Serializable event, boolean convertToJson) throws JMSException { + // Clone connection factory + if (connectionString == null) connectionString = properties.getBrokerUrlForConsumer(); + ConnectionFactory connectionFactory = brokerConfig.getConnectionFactoryFor(connectionString); + + // Create a Connection + log.trace("BrokerCepService._publishEvent(): Connection info: conn-string={}, username={}, password={}", + connectionString, username, passwordUtil.encodePassword(password)); + Connection connection = StringUtils.isBlank(username) + ? connectionFactory.createConnection() + : connectionFactory.createConnection(username, password); + connection.start(); + + // Publish event + _publishEvent(connection, destinationName, event, convertToJson); + + // Clean up + connection.close(); + } + + private synchronized void _publishEvent(Connection connection, String destinationName, Serializable event, boolean convertToJson) throws JMSException { + log.trace("BrokerCepService._publishEvent(): Connection given: {}", connection); + + // Create a Session + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + // Publish event + _publishEvent(session, destinationName, event, convertToJson); + + // Clean up + session.close(); + } + + private synchronized void _publishEvent(Session session, String destinationName, Serializable event, boolean convertToJson) throws JMSException { + log.trace("BrokerCepService._publishEvent(): Session: {}", session); + + // Create the destination (Topic or Queue) + log.trace("BrokerCepService._publishEvent(): Destination info: name={}", destinationName); + //Destination destination = session.createQueue( destinationName ); + Destination destination = session.createTopic(destinationName); + + // Create a MessageProducer from the Session to the Topic or Queue + MessageProducer producer = session.createProducer(destination); + producer.setDeliveryMode(javax.jms.DeliveryMode.NON_PERSISTENT); + + // Create a message + //ObjectMessage message = session.createObjectMessage(event); + String payload = convertToJson ? gson.toJson(event) : (event!=null ? event.toString() : null); + log.trace("BrokerCepService.publishEvent(): Message payload: topic={}, convert-to-json={}, payload={}", destination, convertToJson, payload); + TextMessage message = session.createTextMessage(payload); + + // Set message properties + addEventPropertiesToMessage(event, message); + + // Tell the producer to send the message + long hash = message.hashCode(); + //log.info("BrokerCepService.publishEvent(): Sending message: connection={}, username={}, destination={}, hash={}, payload={}", connectionString, username, destinationName, hash, event); + log.trace("BrokerCepService.publishEvent(): Sending message: destination={}, hash={}, payload={}", destinationName, hash, event); + producer.send(message); + //log.info("BrokerCepService.publishEvent(): Message sent: connection={}, username={}, destination={}, hash={}, payload={}", connectionString, username, destinationName, hash, event); + log.debug("BrokerCepService.publishEvent(): Message sent: destination={}, hash={}, payload={}", destinationName, hash, event); + } + + private void addEventPropertiesToMessage(Serializable event, Message message) { + if (event instanceof EventMap) { + Map eventProperties = ((EventMap) event).getEventProperties(); + if (eventProperties!=null) { + eventProperties.forEach((pName,pValue)->{ + try { + message.setStringProperty(pName, pValue!=null ? pValue.toString() : null); + } catch (JMSException e) { + log.warn("BrokerCepService.publishEvent(): Exception while setting event property. Skipping it: name={}, value={}", pName, pValue); + log.debug("BrokerCepService.publishEvent(): Exception while setting event property. Skipping it: name={}, value={}, EXCEPTION:\n", pName, pValue, e); + } + }); + } + } + } + + private String getAddressFromBrokerUrl(String url) { + return StringUtils.substringBetween(url, "://",":"); + } + + private boolean _isLocalBrokerUrl(String url) { + if (StringUtils.isEmpty(url)) { + log.debug("BrokerCepService._isLocalBrokerUrl(): url={}, is-local=true", url); + return true; + } + log.trace("BrokerCepService._isLocalBrokerUrl(): url={}", url); + try { + String address = getAddressFromBrokerUrl(url); + boolean isLocal = NetUtil.isLocalAddress(address); + log.debug("BrokerCepService._isLocalBrokerUrl(): url={}, address={}, is-local={}", url, address, isLocal); + return isLocal; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public void setBrokerCredentials(String username, String password) { + brokerConfig.setBrokerUsername(username); + brokerConfig.setBrokerPassword(password); + log.info("BrokerCepService.setBrokerCredentials(): Broker credentials set: username={}, password={}", + username, passwordUtil.encodePassword(password)); + } + + public String getBrokerUsername() { + return brokerConfig.getBrokerLocalUserUsername(); + } + + public String getBrokerPassword() { + return brokerConfig.getBrokerLocalUserPassword(); + } + + public KeyStore getBrokerTruststore() { + return brokerConfig.getBrokerTruststore(); + } + + public String getBrokerCertificate() { + return brokerConfig.getBrokerCertificate(); + } + + public Certificate addOrReplaceCertificateInTruststore(String alias, String certPem) throws Exception { + log.trace("BrokerCepService.addOrReplaceCertificateInTruststore(): BEGIN: alias={}, cert-PEM=\n{}", alias, certPem); + if (StringUtils.isNotEmpty(certPem)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + try (InputStream inputStream = new ByteArrayInputStream(certPem.getBytes(Charset.forName("UTF-8")))) { + Certificate cert = cf.generateCertificate(inputStream); + log.debug("BrokerCepService.addOrReplaceCertificateInTruststore(): X509 Certificate: {}", + ((X509Certificate) cert).getSubjectX500Principal().getName()); + return addOrReplaceCertificateInTruststore(alias, cert); + } + } else { + log.debug("BrokerCepService.addOrReplaceCertificateInTruststore(): PEM certificate is empty. Returning 'null'"); + return null; + } + } + + public Certificate addOrReplaceCertificateInTruststore(String alias, Certificate cert) throws Exception { + log.trace("BrokerCepService.addOrReplaceCertificateInTruststore(): BEGIN: alias={}, cert=\n{}", alias, cert); + brokerConfig.getBrokerTruststore().setCertificateEntry(alias, cert); + brokerConfig.writeTruststore(); + log.debug("BrokerCepService.addOrReplaceCertificateInTruststore(): Certificate added with alias: {}", alias); + log.debug("BrokerCepService.addOrReplaceCertificateInTruststore(): New Truststore certificates: {}", + KeystoreUtil.getCertificateAliases(brokerConfig.getBrokerTruststore())); + return cert; + } + + public void deleteCertificateFromTruststore(String alias) throws KeyStoreException { + log.trace("BrokerCepService.deleteCertificateFromTruststore(): BEGIN: alias={}", alias); + brokerConfig.getBrokerTruststore().deleteEntry(alias); + log.debug("BrokerCepService.deleteCertificateFromTruststore(): Deleted certificate with alias: {}", alias); + log.debug("BrokerCepService.addOrReplaceCertificateInTruststore(): New Truststore certificates: {}", + KeystoreUtil.getCertificateAliases(brokerConfig.getBrokerTruststore())); + } + + public Map getBrokerCepStatistics() { + Map bcepStats = new HashMap<>(); + bcepStats.put("count-event-local-publish-success", BrokerCepStatementSubscriber.getLocalPublishSuccessCounter()); + bcepStats.put("count-event-local-publish-failure", BrokerCepStatementSubscriber.getLocalPublishFailureCounter()); + bcepStats.put("count-event-forwards-success", BrokerCepStatementSubscriber.getForwardSuccessCounter()); + bcepStats.put("count-event-forwards-failure", BrokerCepStatementSubscriber.getForwardFailureCounter()); + bcepStats.put("count-total-events", BrokerCepConsumer.getEventCounter()); + bcepStats.put("count-total-events-text", BrokerCepConsumer.getTextEventCounter()); + bcepStats.put("count-total-events-object", BrokerCepConsumer.getObjectEventCounter()); + bcepStats.put("count-total-events-other", BrokerCepConsumer.getOtherEventCounter()); + bcepStats.put("count-total-events-failures", BrokerCepConsumer.getEventFailuresCounter()); + + bcepStats.put("latest-events", eventCache.asList()); + + return bcepStats; + } + + public void clearBrokerCepStatistics() { + BrokerCepStatementSubscriber.clearCounters(); + BrokerCepConsumer.clearCounters(); + log.debug("BrokerCepService.clearBrokerCepStatistics(): broker-CEP statistics cleared"); + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepStatementSubscriber.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepStatementSubscriber.java new file mode 100644 index 0000000..ce359bf --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/BrokerCepStatementSubscriber.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep; + +import gr.iccs.imu.ems.brokercep.cep.StatementSubscriber; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.util.GroupingConfiguration; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +@Slf4j +@Getter +@AllArgsConstructor +@RequiredArgsConstructor +public class BrokerCepStatementSubscriber implements StatementSubscriber { + private final static AtomicLong counterLocalPublishSuccess = new AtomicLong(0); + private final static AtomicLong counterLocalPublishFailure = new AtomicLong(0); + private final static AtomicLong counterForwardSuccess = new AtomicLong(0); + private final static AtomicLong counterForwardFailure = new AtomicLong(0); + + private final String name; + private final String topic; + private final String statement; + private final BrokerCepService brokerCep; + private final PasswordUtil passwordUtil; + @Setter + private Set forwardToGroupings; + + public void update(Map eventMap) { + log.trace("BrokerCepStatementSubscriber.update(): INPUT: {}", eventMap); + EventMap.checkEvent(eventMap); + publishToLocalBroker(eventMap); + forwardToGroupings(eventMap); + } + + protected void publishToLocalBroker(Map eventMap) { + log.info("- New event received: subscriber={}, topic={}, payload={}", name, topic, eventMap); + String localBrokerUrl = brokerCep.getBrokerCepProperties().getBrokerUrlForConsumer(); + String username = brokerCep.getBrokerUsername(); + String password = brokerCep.getBrokerPassword(); + String passwordEncoded = passwordUtil.encodePassword(password); + try { + // Queue new event for publishing to Local Broker topic + EventForwarder.getInstance().addLocalPublishTask(this, topic, eventMap, ()->countLocalPublish(true), ()->countLocalPublish(false)); + log.trace("- Event queued for publishing to local broker: subscriber={}, local-broker={}, username={}, password={}, topic={}, payload={}", + name, localBrokerUrl, username, passwordEncoded, topic, eventMap); + } catch (Exception ex) { + log.error("- New event: ERROR while queueing event for publishing to local broker: subscriber={}, local-broker={}, username={}, password={}, topic={}, exception=", + name, localBrokerUrl, username, passwordEncoded, topic, ex); + countLocalPublish(false); + } + } + + protected void forwardToGroupings(Map eventMap) { + // Queue event for forwarding to the next grouping(s) + log.trace("- Forwarding event to groupings: subscriber={}, forward-to-groupings={}, payload={}", + name, forwardToGroupings, eventMap); + if (forwardToGroupings==null) + return; + for (GroupingConfiguration.BrokerConnectionConfig fwdToGrouping : forwardToGroupings) { + try { + EventForwarder.getInstance().addEventForwardTask(this, fwdToGrouping, topic, eventMap, ()->countForward(true), ()->countForward(false)); + log.debug("- Event queued for forwarding to grouping: subscriber={}, forward-to-grouping={}, topic={}, payload={}", + name, fwdToGrouping, topic, eventMap); + } catch (Exception ex) { + log.error("- ERROR while queuing event in forward queue: subscriber={}, forward-to-groupings={}, payload={}, exception: ", + name, forwardToGroupings, eventMap, ex); + countForward(false); + } + } + } + + private void countLocalPublish(boolean success) { + if (success) counterLocalPublishSuccess.incrementAndGet(); + else counterLocalPublishFailure.incrementAndGet(); + } + + private void countForward(boolean success) { + if (success) counterForwardSuccess.incrementAndGet(); + else counterForwardFailure.incrementAndGet(); + } + + public static long getLocalPublishSuccessCounter() { return counterLocalPublishSuccess.get(); } + public static long getLocalPublishFailureCounter() { return counterLocalPublishFailure.get(); } + public static long getForwardSuccessCounter() { return counterForwardSuccess.get(); } + public static long getForwardFailureCounter() { return counterForwardFailure.get(); } + public static synchronized void clearCounters() { + counterLocalPublishSuccess.set(0L); + counterLocalPublishFailure.set(0L); + counterForwardSuccess.set(0L); + counterForwardFailure.set(0L); + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/EventCache.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/EventCache.java new file mode 100644 index 0000000..754d77f --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/EventCache.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep; + +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EventCache implements InitializingBean { + public final static int DEFAULT_EVENT_CACHE_SIZE = 100; + + private final BrokerCepProperties properties; + private final AtomicLong cacheCounter = new AtomicLong(0); + private ArrayBlockingQueue messageCache; + private boolean enabled; + + @Override + public void afterPropertiesSet() throws Exception { + enabled = properties==null || properties.isEventCacheEnabled(); + if (properties!=null && properties.getEventCacheSize()==0) enabled = false; + if (!enabled) return; + + int s = properties!=null ? properties.getEventCacheSize() : -1; + if (s<0) s = DEFAULT_EVENT_CACHE_SIZE; + messageCache = new ArrayBlockingQueue<>(s); + } + + public List asList() { + return enabled ? new ArrayList<>(messageCache) : Collections.emptyList(); + } + + public synchronized void clearCache() { + clearCache(false); + } + + public synchronized void clearCache(boolean resetCounter) { + if (!enabled) return; + messageCache.clear(); + cacheCounter.set(0); + } + + public void cacheEvent(EventMap eventMap, String destination) { + cacheEvent(eventMap, eventMap.getEventProperties(), destination); + } + + public void cacheEvent(Object event, Map properties, String destination) { + if (!enabled) return; + CacheEntry entry; + synchronized (cacheCounter) { + try { + while (messageCache.remainingCapacity() == 0) + messageCache.poll(); + entry = new CacheEntry( + destination, + cacheCounter.getAndIncrement(), + System.currentTimeMillis()); + if (!messageCache.offer(entry)) { + log.warn("EventCache.cacheEvent: Failed to cache event. Cache is full: size={}", messageCache.size()); + return; + } + } catch (Throwable e) { + log.warn("EventCache.cacheEvent: Exception while caching event: ", e); + return; + } + } + entry.payload = event; + entry.properties = properties.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, p -> p.getValue()!=null ? p.getValue().toString() : "" + )); + } + + @ToString + @RequiredArgsConstructor + public static class CacheEntry implements Serializable { + public Object payload; + public Map properties; + public final String destination; + public final long counter; + public final long timestamp; + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/EventForwarder.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/EventForwarder.java new file mode 100644 index 0000000..17437a3 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/EventForwarder.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep; + +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import gr.iccs.imu.ems.util.GroupingConfiguration; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EventForwarder implements InitializingBean, Runnable { + @Getter @Setter + private static EventForwarder instance; + + private final BrokerCepProperties properties; + private final BrokerCepService brokerCepService; + private final LinkedBlockingDeque eventForwardingQueue = new LinkedBlockingDeque<>(); + + @Override + public void afterPropertiesSet() throws Exception { + if (instance==null) instance = this; + Executors.newFixedThreadPool(1).submit(this); + log.info("EventForwarder: Starting event publish/forward worker"); + } + + public void addEventForwardTask(@NonNull BrokerCepStatementSubscriber sender, @NonNull GroupingConfiguration.BrokerConnectionConfig brokerConnectionConfig, @NonNull String topic, @NonNull Map eventMap, Runnable success, Runnable failure) { + boolean isLocalPublish = + brokerCepService.getBrokerCepProperties().getBrokerUrlForConsumer() + .equals(brokerConnectionConfig.getUrl()); + eventForwardingQueue.add(new EventForwardTask(sender, isLocalPublish, brokerConnectionConfig, topic, eventMap, success, failure)); + log.debug("EventForwarder: {} task in the queue", eventForwardingQueue.size()); + } + + public void addEventForwardTask(@NonNull BrokerCepStatementSubscriber sender, String grouping, String brokerUrl, String certificate, String username, String password, @NonNull String topic, @NonNull Map eventMap, Runnable success, Runnable failure) { + GroupingConfiguration.BrokerConnectionConfig brokerConnectionConfig = + new GroupingConfiguration.BrokerConnectionConfig(grouping, brokerUrl, certificate, username, password); + addEventForwardTask(sender, brokerConnectionConfig, topic, eventMap, success, failure); + } + + public void addLocalPublishTask(@NonNull BrokerCepStatementSubscriber sender, @NonNull String topic, @NonNull Map eventMap, Runnable success, Runnable failure) { + String brokerUrl = brokerCepService.getBrokerCepProperties().getBrokerUrlForConsumer(); + String username = brokerCepService.getBrokerUsername(); + String password = brokerCepService.getBrokerPassword(); + GroupingConfiguration.BrokerConnectionConfig brokerConnectionConfig = + new GroupingConfiguration.BrokerConnectionConfig(null, brokerUrl, null, username, password); + eventForwardingQueue.add(new EventForwardTask(sender, true, brokerConnectionConfig, topic, eventMap, success, failure)); + log.debug("EventForwarder: {} task in the queue", eventForwardingQueue.size()); + } + + @Override + public void run() { + long delay = properties.getEventForwarderLoopDelay(); + if (delay<10L) delay = 100L; + + while (true) { + try { + processEventForwardTask(eventForwardingQueue.take()); + waitFor(delay); + } catch (Throwable t) { + log.warn("EventForwarder: Exception thrown in task processing loop: ", t); + } + } + } + + private void waitFor(long delayInMillis) { + try { + Thread.sleep(delayInMillis); + } catch (InterruptedException e) { + log.warn("EventForwarder: waitFor: Interrupted: ", e); + } + } + + private void processEventForwardTask(EventForwardTask task) { + String senderName = task.getSender().getName(); + String topic = task.getTopic(); + Map eventMap = task.getEventMap(); + + // Check if max task processing duration has been exceeded + long duration = System.currentTimeMillis() - task.getCreation(); + if (properties.getMaxEventForwardDuration()>0 && duration > properties.getMaxEventForwardDuration()) { + log.error("- Max event publish/forward duration exceeded. Dropping event: subscriber={}, forward-to-groupings={}, topic={}, payload={}", + senderName, task.getBrokerConnectionConfig(), topic, eventMap); + + runIfNotNull(task.getFailure()); + return; + } + + // Process event publish/forward task + try { + String brokerUrl = task.getBrokerConnectionConfig().getUrl(); + String username = task.getBrokerConnectionConfig().getUsername(); + String password = task.getBrokerConnectionConfig().getPassword(); + + if (task.isLocalPublish()) { + // Log start of event send to the local broker + log.trace("- Publishing event to local broker: subscriber={}, local-broker={}, username={}, password={}, topic={}, retry={}, payload={}", + senderName, brokerUrl, username, "passwordEncoded", topic, task.getRetries(), eventMap); + } else { + log.trace("- Checking forward broker configuration before event send: subscriber={}, local-broker={}, username={}, password={}, topic={}, retry={}, payload={}", + senderName, brokerUrl, username, "passwordEncoded", topic, task.getRetries(), eventMap); + String targetGrouping = task.getBrokerConnectionConfig().getGrouping(); + log.trace("- Target grouping: {}", targetGrouping); + + // Check if sender forwards have been cleared (indicating that this node became an aggregator) + boolean configChanged = false; + boolean forwardsExist = task.getSender().getForwardToGroupings() != null && task.getSender().getForwardToGroupings().size() > 0; + log.trace("- Forwards exist: {}", forwardsExist); + + if (forwardsExist) { + // Get forward broker configuration from the sender + GroupingConfiguration.BrokerConnectionConfig bcc = + task.getSender().getForwardToGroupings().stream() + .filter(f -> f.getGrouping().equals(targetGrouping)) + .findAny().orElse(null); + log.trace("- Selected BrokerConnectionConfig: {}", bcc); + + // Log any changes in forward broker config + String brokerUrl2 = bcc!=null ? bcc.getUrl() : null; + String username2 = bcc!=null ? bcc.getUsername() : null; + String password2 = bcc!=null ? bcc.getPassword() : null; + + if (!brokerUrl.equals(brokerUrl2)) { + log.warn("- Forward broker config changed: sender: {}, broker-url: {} -> {}, event: {}", senderName, brokerUrl, brokerUrl2, task.getEventMap()); + brokerUrl = brokerUrl2; + configChanged = true; + } + if (!username.equals(username2)) { + log.warn("- Forward broker config changed: sender: {}, username: {} -> {}, event: {}", senderName, username, username2, task.getEventMap()); + username = username2; + configChanged = true; + } + if (!password.equals(password2)) { + log.warn("- Forward broker config changed: sender: {}, password: ******** -> ********, event: {}", senderName, task.getEventMap()); + password = password2; + configChanged = true; + } + } else { + log.warn("- Forwards removed for topic and grouping. Using local broker: topic={}, grouping={}, sender={}, event={}", task.getTopic(), targetGrouping, senderName, task.getEventMap()); + + brokerUrl = brokerCepService.getBrokerCepProperties().getBrokerUrlForConsumer(); + username = brokerCepService.getBrokerUsername(); + password = brokerCepService.getBrokerPassword(); + configChanged = true; + } + + // Log start of event send to forward broker + if (configChanged) + log.debug("- Forwarding event to grouping: CONFIG-CHANGED: subscriber={}, forward-to-grouping={}, url={}, username={}, topic={}, retry={}, payload={}", + senderName, task.getBrokerConnectionConfig(), brokerUrl, username, topic, task.getRetries(), eventMap); + else + log.debug("- Forwarding event to grouping: subscriber={}, forward-to-grouping={}, url={}, username={}, topic={}, retry={}, payload={}", + senderName, task.getBrokerConnectionConfig(), brokerUrl, username, topic, task.getRetries(), eventMap); + } + + // Update retry info and try sending event + task.newRetry(); + brokerCepService.publishEvent(brokerUrl, username, password, topic, eventMap); + task.completed(); + + // Log successful event send + if (task.isLocalPublish()) { + log.debug("- Event published to local broker: subscriber={}, local-broker={}, username={}, topic={}, payload={}, duration={}ms", + senderName, brokerUrl, username, topic, eventMap, task.getTotalDuration()); + } else { + log.debug("- Event forwarded to grouping: subscriber={}, forwarded-to-grouping={}, url={}, username={}, topic={}, payload={}, duration={}ms", + senderName, task.brokerConnectionConfig, brokerUrl, username, topic, eventMap, task.getTotalDuration()); + } + + // Run successful event send callback + runIfNotNull(task.getSuccess()); + + } catch (IllegalArgumentException ex) { + // Event with errors + log.error("- Event contains errors. Will not retry to send it: Error while sending event: subscriber={}, forward-to-groupings={}, topic={}, retry={}, duration={}ms, payload={}, exception: ", + senderName, task.getBrokerConnectionConfig(), topic, task.getRetries() - 1, task.getTotalDuration(), eventMap, ex); + + runIfNotNull(task.getFailure()); + + } catch (Exception ex) { + // Increase retry count and log failed event send + task.increaseRetries(); + log.error("- Error while sending event: subscriber={}, forward-to-groupings={}, topic={}, retry={}, duration={}ms, payload={}, exception: ", + senderName, task.getBrokerConnectionConfig(), topic, task.getRetries()-1, task.getTotalDuration(), eventMap, ex); + + // Check if retries exceeded limits. If not then put event back in the queue. + if (properties.getMaxEventForwardRetries()>=0 && task.getRetries() > properties.getMaxEventForwardRetries()) { + log.error("- Max event publish/forward retries exceeded. Dropping event: subscriber={}, forward-to-groupings={}, topic={}, payload={}", + senderName, task.getBrokerConnectionConfig(), topic, eventMap); + + runIfNotNull(task.getFailure()); + + } else + if (properties.getMaxEventForwardDuration()>0 && task.getTotalDuration() > properties.getMaxEventForwardDuration()) { + log.error("- Max event publish/forward duration exceeded. Dropping event: subscriber={}, forward-to-groupings={}, topic={}, payload={}", + senderName, task.getBrokerConnectionConfig(), topic, eventMap); + + runIfNotNull(task.getFailure()); + + } else { + // Retry limits not exceeded. Put event back in the queue + eventForwardingQueue.add(task); + log.debug("- Event placed back in queue: subscriber={}, forward-to-groupings={}, topic={}, payload={}", + senderName, task.getBrokerConnectionConfig(), topic, eventMap); + } + } + } + + protected void runIfNotNull(Runnable r) { + if (r==null) return; + r.run(); + } + + @Getter + @RequiredArgsConstructor + protected static class EventForwardTask { + @NonNull private final BrokerCepStatementSubscriber sender; + private final boolean localPublish; + @NonNull private final GroupingConfiguration.BrokerConnectionConfig brokerConnectionConfig; + @NonNull private final String topic; + @NonNull private final Map eventMap; + private final Runnable success; + private final Runnable failure; + private final long creation = System.currentTimeMillis(); + + private long lastRetryStart; + private long lastRetryEnd; + private boolean completed; + private int retries = 0; + + public void newRetry() { + if (completed) return; + lastRetryStart = System.currentTimeMillis(); + } + + public void completed() { + if (completed) return; + completed = true; + lastRetryEnd = System.currentTimeMillis(); + } + + public void increaseRetries() { + if (completed) return; + lastRetryEnd = System.currentTimeMillis(); + ++retries; + } + + public long getLastRetryDuration() { + return lastRetryEnd - lastRetryStart; + } + + public long getTotalDuration() { + return lastRetryEnd - creation; + } + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/BrokerAdvisoryWatcher.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/BrokerAdvisoryWatcher.java new file mode 100644 index 0000000..8503a04 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/BrokerAdvisoryWatcher.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker; + +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.BrokerService; +import org.apache.activemq.command.ActiveMQDestination; +import org.apache.activemq.command.ActiveMQMessage; +import org.apache.activemq.command.DataStructure; +import org.apache.activemq.command.DestinationInfo; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import javax.jms.*; +import java.time.Instant; + +@Slf4j +@Service +@ConditionalOnProperty(name="brokercep.enable-advisory-watcher", matchIfMissing = true) +@RequiredArgsConstructor +public class BrokerAdvisoryWatcher implements MessageListener, InitializingBean, ApplicationListener { + private final BrokerService brokerService; // Added in order to ensure that BrokerService will be instantiated first + private final BrokerConfig brokerConfig; + private final BrokerCepService brokerCepService; + private final BrokerCepProperties properties; + + private ConnectionFactory connectionFactory; + + private final PasswordUtil passwordUtil; + private final TaskScheduler taskScheduler; + + private Connection connection; + private Session session; + private boolean shuttingDown; + + @Override + public void afterPropertiesSet() { + log.debug("BrokerAdvisoryWatcher: afterPropertiesSet: BrokerCepProperties: {}", brokerCepService.getBrokerCepProperties()); + initialize(); + } + + protected void initialize() { + log.debug("BrokerAdvisoryWatcher.init(): Initializing instance..."); + try { + // close previous session and connection + closeConnection(); + + // If an alternative Broker URL is provided for consumer, it will be used + if (connectionFactory==null) { + connectionFactory = brokerConfig.getConnectionFactoryForConsumer(); + } + + // If authentication is enabled get credentials + boolean usesAuthentication = brokerCepService.getBrokerCepProperties().isAuthenticationEnabled(); + String username = brokerCepService.getBrokerUsername(); + String password = brokerCepService.getBrokerPassword(); + log.debug("BrokerAdvisoryWatcher.init(): uses-authentication={}, username={}, password={}", + usesAuthentication, username, passwordUtil.encodePassword(password)); + + // Create and start new connection + this.connection = usesAuthentication + ? connectionFactory.createConnection(username, password) + : connectionFactory.createConnection(); + connection.setExceptionListener(e -> { + if (!shuttingDown) { + log.warn("BrokerAdvisoryWatcher: Connection exception listener: Exception caught: ", e); + initialize(); + } + }); + this.connection.start(); + + // Create a new session, and new consumer for topic + this.session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + Topic topic = session.createTopic("ActiveMQ.Advisory.>"); + MessageConsumer consumer = session.createConsumer(topic); + consumer.setMessageListener( this ); + + log.debug("BrokerAdvisoryWatcher.init(): Initializing instance... done"); + } catch (Exception ex) { + log.error("BrokerAdvisoryWatcher.init(): EXCEPTION: while retry in {} seconds:", properties.getAdvisoryWatcherInitRetryDelay(), ex); + final BrokerAdvisoryWatcher _this = this; + taskScheduler.schedule(_this::initialize, Instant.now().plusSeconds(properties.getAdvisoryWatcherInitRetryDelay())); + } + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + log.info("BrokerAdvisoryWatcher is shutting down"); + shuttingDown = true; + } + + private void closeConnection() { + // close previous session and connection + try { + if (session != null) { + session.close(); + log.debug("BrokerCepConsumer.closeConnection(): Closed pre-existing sessions"); + } + } catch (Exception e) { + log.warn("BrokerCepConsumer.closeConnection(): Exception while closing old session: ", e); + } + try { + if (connection != null) { + connection.close(); + log.debug("BrokerCepConsumer.closeConnection(): Closed pre-existing connection"); + } + } catch (Exception e) { + log.warn("BrokerCepConsumer.closeConnection(): Exception while closing old connection: ", e); + } + session = null; + connection = null; + } + + @Override + public void onMessage(Message message) { + try { + log.trace("BrokerAdvisoryWatcher.onMessage(): {}", message); + ActiveMQMessage mesg = (ActiveMQMessage) message; + ActiveMQDestination messageDestination = mesg.getDestination(); + log.trace("BrokerAdvisoryWatcher.onMessage(): advisory-message-source={}", messageDestination); + + DataStructure ds = mesg.getDataStructure(); + log.trace("BrokerAdvisoryWatcher.onMessage(): advisory-message-data-structure={}", ds==null ? null : ds.getClass().getSimpleName()); + if (ds!=null) { + // Advisory event + processAdvisoryMessage(ds); + } else { + // Non-advisory event + processPlainMessage(mesg); + } + } catch (Exception ex) { + log.error("BrokerAdvisoryWatcher.onMessage(): EXCEPTION: ", ex); + } + } + + private void processPlainMessage(ActiveMQMessage mesg) throws JMSException { + if (mesg instanceof TextMessage) { + TextMessage txtMesg = (TextMessage) mesg; + String topicName = mesg.getDestination().getPhysicalName(); + log.trace("BrokerAdvisoryWatcher.onMessage(): Text Message received: topic={}, message={}", topicName, txtMesg.getText()); + } else { + String topicName = mesg.getDestination().getPhysicalName(); + log.trace("BrokerAdvisoryWatcher.onMessage(): Non-text Message received: topic={}, type={}", topicName, mesg.getClass().getName()); + } + } + + private void processAdvisoryMessage(DataStructure ds) throws JMSException { + if (ds instanceof DestinationInfo) { + DestinationInfo info = (DestinationInfo) ds; + ActiveMQDestination destination = info.getDestination(); + boolean isAdd = info.isAddOperation(); + boolean isDel = info.isRemoveOperation(); + log.debug("BrokerAdvisoryWatcher.onMessage(): Received a DestinationInfo message: destination={}, is-queue={}, is-topic={}, is-add={}, is-del={}", + destination, destination.isQueue(), destination.isTopic(), isAdd, isDel); + + // Subscribe to topic + if (isAdd) { + String topicName = destination.getPhysicalName(); + log.debug("BrokerAdvisoryWatcher.onMessage(): Subscribing to topic: {}", topicName); + + MessageConsumer consumer = session.createConsumer(destination); + consumer.setMessageListener(this); + } + /*if (isDel) { + String topicName = destination.getPhysicalName(); + log.info("BrokerAdvisoryWatcher.onMessage(): Leaving topic: {}", topicName); + }*/ + + } else { + log.trace("BrokerAdvisoryWatcher.onMessage(): Message ignored"); + } + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/BrokerConfig.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/BrokerConfig.java new file mode 100644 index 0000000..48228ea --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/BrokerConfig.java @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker; + +import gr.iccs.imu.ems.brokercep.broker.interceptor.AbstractMessageInterceptor; +import gr.iccs.imu.ems.brokercep.event.EventRecorder; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import gr.iccs.imu.ems.util.KeystoreUtil; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.ActiveMQSslConnectionFactory; +import org.apache.activemq.broker.BrokerPlugin; +import org.apache.activemq.broker.BrokerService; +import org.apache.activemq.broker.SslBrokerService; +import org.apache.activemq.broker.inteceptor.MessageInterceptorRegistry; +import org.apache.activemq.broker.jmx.ManagementContext; +import org.apache.activemq.pool.PooledConnectionFactory; +import org.apache.activemq.security.AuthenticationUser; +import org.apache.activemq.security.SimpleAuthenticationPlugin; +import org.apache.activemq.usage.MemoryUsage; +import org.apache.activemq.usage.SystemUsage; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileSystemResource; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import javax.jms.ConnectionFactory; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.security.KeyStore; +import java.util.*; +import java.util.stream.Collectors; + +//import org.apache.activemq.security.JaasAuthenticationPlugin; + + +@Slf4j +@Service +@EnableJms +@Configuration +@RequiredArgsConstructor +public class BrokerConfig implements InitializingBean { + + private final static int LOCAL_ADMIN_INDEX = 0; + private final static int LOCAL_USER_INDEX = 1; + private final static String LOCAL_ADMIN_PREFIX = "admin-local-"; + private final static String LOCAL_USER_PREFIX = "user-local-"; + private final static int USERNAME_RANDOM_PART_LENGTH = 10; + private final static int PASSWORD_LENGTH = 20; + + private final BrokerCepProperties properties; + private final PasswordUtil passwordUtil; + private final ApplicationContext applicationContext; + + private SimpleAuthenticationPlugin brokerAuthenticationPlugin; + private SimpleBrokerAuthorizationPlugin brokerAuthorizationPlugin; + private ArrayList userList; + private String brokerLocalAdmin; + private String brokerLocalAdminPassword; + private String brokerUsername; + private String brokerPassword; + private String brokerCert; + + private KeyStore truststore; + + private final HashMap connectionFactoryCache = new HashMap<>(); + + private final TaskScheduler scheduler; + @Getter + private EventRecorder eventRecorder; + + @Override + public void afterPropertiesSet() throws Exception { + _initializeSecurity(); + _initializeEventRecorder(); + } + + protected synchronized void _initializeSecurity() throws Exception { + log.debug("BrokerConfig._initializeSecurity(): Initializing broker security: initialize-authentication={}, initialize-authorization={}", + properties.isAuthenticationEnabled(), properties.isAuthorizationEnabled()); + + // initialize authentication + if (properties.isAuthenticationEnabled()) { + userList = new ArrayList<>(); + + // initialize local admin credentials + brokerLocalAdmin = LOCAL_ADMIN_PREFIX + RandomStringUtils.randomAlphanumeric(USERNAME_RANDOM_PART_LENGTH); + brokerLocalAdminPassword = RandomStringUtils.randomAlphanumeric(PASSWORD_LENGTH); + userList.add(new AuthenticationUser(brokerLocalAdmin, brokerLocalAdminPassword, SimpleBrokerAuthorizationPlugin.ADMIN_GROUP)); + log.debug("BrokerConfig._initializeSecurity(): Initialized local admin: {} / {}", + brokerLocalAdmin, passwordUtil.encodePassword(brokerLocalAdminPassword)); + + // initialize broker user credentials + brokerUsername = LOCAL_USER_PREFIX+ RandomStringUtils.randomAlphanumeric(USERNAME_RANDOM_PART_LENGTH); + brokerPassword = RandomStringUtils.randomAlphanumeric(PASSWORD_LENGTH); + userList.add(new AuthenticationUser(brokerUsername, brokerPassword, SimpleBrokerAuthorizationPlugin.RO_USER_GROUP)); + log.debug("BrokerConfig._initializeSecurity(): Initialized broker user: {} / {}", + brokerUsername, passwordUtil.encodePassword(brokerPassword)); + + // initialize additional user credentials from configuration + if (StringUtils.isNotBlank(properties.getAdditionalBrokerCredentials())) { + for (String extraUserCred : properties.getAdditionalBrokerCredentials().split(",")) { + String[] cred = extraUserCred.split("/", 2); + String username = cred[0].trim(); + String password = cred.length > 1 ? cred[1].trim() : ""; + userList.add(new AuthenticationUser(username, password, SimpleBrokerAuthorizationPlugin.RW_USER_GROUP)); + log.debug("BrokerConfig._initializeSecurity(): Initialized additional broker user from configuration: {} / {}", + username, passwordUtil.encodePassword(password)); + } + } + + // initialize Broker authentication plugin + SimpleAuthenticationPlugin sap = new SimpleAuthenticationPlugin(); //new JaasAuthenticationPlugin() + sap.setAnonymousAccessAllowed(false); + sap.setUsers(userList); + brokerAuthenticationPlugin = sap; + + if (log.isDebugEnabled()) { + log.debug("BrokerConfig._initializeSecurity(): Initialized broker authentication plugin: anonymous-access={}, user-list={}", + sap.isAnonymousAccessAllowed(), sap.getUserPasswords().keySet() + ); + } + } + + // initialize authorization (requires authentication being enabled) + if (properties.isAuthorizationEnabled()) { + if (properties.isAuthenticationEnabled()) { + // initialize Broker authorization plugin + brokerAuthorizationPlugin = new SimpleBrokerAuthorizationPlugin(); + log.debug("BrokerConfig._initializeSecurity(): Initialized broker authorization plugin"); + } else { + log.error("BrokerConfig._initializeSecurity(): Authorization will not be configured because authentication is not enabled"); + } + } + + // Initialize Key pair and Certificate for SSL broker + if (getBrokerUrl().startsWith("ssl")) { + log.debug("BrokerConfig._initializeSecurity(): Initializing Broker key pair and certificate..."); + initializeKeyPairAndCert(); + log.debug("BrokerConfig._initializeSecurity(): Broker key pair and certificate initialization has been completed"); + } else { + log.debug("BrokerConfig._initializeSecurity(): Broker key pair and certificate NOT initialized"); + } + } + + private void initializeKeyPairAndCert() throws Exception { + log.debug("BrokerConfig.initializeKeyAndCert(): BrokerCepProperties: {}", properties); + log.debug("BrokerConfig.initializeKeyAndCert(): Initializing keystore, truststore and certificate for Broker-SSL..."); + KeystoreUtil.initializeKeystoresAndCertificate(properties.getSsl(), passwordUtil); + + log.trace("BrokerConfig.initializeKeyAndCert(): Retrieving certificate for Broker-SSL: file={}, type={}, password={}, alias={}...", + properties.getSsl().getKeystoreFile(), properties.getSsl().getKeystoreType(), + passwordUtil.encodePassword(properties.getSsl().getKeystorePassword()), + properties.getSsl().getKeyEntryName()); + log.trace("BrokerConfig.initializeKeyAndCert(): Retrieving certificate for Broker-SSL..."); + this.brokerCert = KeystoreUtil + .getKeystore(properties.getSsl().getKeystoreFile(), properties.getSsl().getKeystoreType(), properties.getSsl().getKeystorePassword()) + .passwordUtil(passwordUtil) + .getEntryCertificateAsPEM(properties.getSsl().getKeyEntryName()); + log.trace("BrokerConfig.initializeKeyAndCert(): Retrieved certificate for Broker-SSL: file={}, type={}, password={}, alias={}, cert=\n{}", + properties.getSsl().getKeystoreFile(), properties.getSsl().getKeystoreType(), + passwordUtil.encodePassword(properties.getSsl().getKeystorePassword()), + properties.getSsl().getKeyEntryName(), this.brokerCert); + log.debug("BrokerConfig.initializeKeyAndCert(): Initializing keystore, truststore and certificate for Broker-SSL... done"); + } + + private void _initializeEventRecorder() throws IOException { + // clear previous event recorder (if any) + if (eventRecorder!=null && !eventRecorder.isClosed()) + eventRecorder.close(); + + // create new event recorder + if (properties.getEventRecorder()!=null) { + if (properties.getEventRecorder().isEnabled()) { + eventRecorder = new EventRecorder(properties.getEventRecorder(), scheduler); + eventRecorder.startRecording(); + } + } + } + + public String getBrokerName() { + log.trace("BrokerConfig.getBrokerName(): broker-name: {}", properties.getBrokerName()); + return properties.getBrokerName(); + } + + public String getBrokerUrl() { + log.trace("BrokerConfig.getBrokerUrl(): broker-url: {}", properties.getBrokerUrl()); + return properties.getBrokerUrl(); + } + + public String getBrokerCertificate() { + log.trace("BrokerConfig.getBrokerCertificate(): Broker certificate (PEM):\n{}", brokerCert); + return brokerCert; + } + + public KeyStore getBrokerTruststore() { return truststore; } + + public String getBrokerLocalAdminUsername() { + return brokerLocalAdmin; + } + + public String getBrokerLocalAdminPassword() { + return brokerLocalAdminPassword; + } + + public String getBrokerLocalUserUsername() { + return brokerUsername; + } + + public String getBrokerLocalUserPassword() { + return brokerPassword; + } + + public void setBrokerUsername(String s) { + if (userList != null) { + brokerUsername = s; + userList.get(LOCAL_USER_INDEX).setUsername(s); // 'userList' contains at least 2 items or is null (see '_initializeSecurity()' method) + brokerAuthenticationPlugin.setUsers(userList); + log.debug("BrokerConfig.setBrokerUsername(): username={}", s); + } else + log.debug("BrokerConfig.setBrokerUsername(): Username not set"); + } + + public void setBrokerPassword(String password) { + if (userList != null) { + brokerPassword = password; + userList.get(LOCAL_USER_INDEX).setPassword(password); + brokerAuthenticationPlugin.setUsers(userList); + log.debug("BrokerConfig.setBrokerPassword(): password={}", passwordUtil.encodePassword(password)); + } else + log.debug("BrokerConfig.setBrokerPassword(): Password not set"); + } + + public BrokerPlugin getBrokerAuthenticationPlugin() { + return brokerAuthenticationPlugin; + } + + public BrokerPlugin getBrokerAuthorizationPlugin() { + return brokerAuthorizationPlugin; + } + + /** + * Creates an embedded JMS server + */ + @Bean + public BrokerService createBrokerService() throws Exception { + + // Create new broker service instance + String brokerUrl = getBrokerUrl(); + log.debug("BrokerConfig: Creating new Broker Service instance: url={}", brokerUrl); + + SslBrokerService brokerService = new SslBrokerService();; + brokerService.setBrokerName(getBrokerName()); + + // Initialize keystore and truststore for broker SSL connectors + KeyManager[] keystore = null; + TrustManager[] truststore = null; + if (secureConnectorsExist()) { + keystore = readKeystore(); + truststore = readTruststore(); + } + + // Start broker connectors + if (properties.getBrokerUrlList()!=null) { + int i = 1; + for (String url : properties.getBrokerUrlList()) { + if (StringUtils.isNotBlank(url)) { + String num = (i==1 ? "st" : (i==2 ? "nd" : "rd")); + log.debug("BrokerConfig: {}{} connector: {}", i++, num, url); + if (isSecureUrl(url)) + // Add an SSL broker connector + brokerService.addSslConnector(url, keystore, truststore, null); + else + // Add a non-SSL broker connector + brokerService.addConnector(url); + } + } + } + + // Set authentication and authorization plugins + List plugins = new ArrayList<>(); + if (getBrokerAuthenticationPlugin()!=null) plugins.add(getBrokerAuthenticationPlugin()); + if (getBrokerAuthorizationPlugin()!=null) plugins.add(getBrokerAuthorizationPlugin()); + if (plugins.size() > 0) { + brokerService.setPlugins(plugins.toArray(new BrokerPlugin[0])); + } + + // Configure broker service instance + log.debug("BrokerConfig: Broker configuration: persistence={}, use-jmx={}, advisory-support={}, use-shutdown-hook={}", + properties.isBrokerPersistenceEnabled(), properties.isBrokerUsingJmx(), properties.isBrokerAdvisorySupportEnabled(), properties.isBrokerUsingShutdownHook()); + brokerService.setPersistent(properties.isBrokerPersistenceEnabled()); + brokerService.setUseJmx(properties.isBrokerUsingJmx()); + brokerService.setUseShutdownHook(properties.isBrokerUsingShutdownHook()); + brokerService.setAdvisorySupport(properties.isBrokerAdvisorySupportEnabled()); + + brokerService.setPopulateJMSXUserID(properties.isBrokerPopulateJmsxUserId()); + brokerService.setEnableStatistics(properties.isBrokerEnableStatistics()); + + // Change the JMX connector port + if (properties.getManagementConnectorPort() > 0) { + if (brokerService.getManagementContext() != null) { + log.debug("BrokerConfig.createBrokerService(): Setting connector port to: {}", properties.getManagementConnectorPort()); + brokerService.getManagementContext().setConnectorPort(properties.getManagementConnectorPort()); + } + } + + // Print Management Context information + try { + log.debug("BrokerConfig.createBrokerService(): Management Context (MC) settings:"); + ManagementContext mc = brokerService.getManagementContext(); + log.debug(" MC: BrokerName: {}", mc.getBrokerName()); + log.debug(" MC: ConnectorHost: {}", mc.getConnectorHost()); + log.debug(" MC: ConnectorPath: {}", mc.getConnectorPath()); + log.debug(" MC: Environment: {}", mc.getEnvironment()); + log.debug(" MC: JmxDomainName: {}", mc.getJmxDomainName()); + log.debug(" MC: RmiServerPort: {}", mc.getRmiServerPort()); + log.debug(" MC: SuppressMBean: {}", mc.getSuppressMBean()); + log.debug(" MC: AllowRemoteAddressInMBeanNames: {}", mc.isAllowRemoteAddressInMBeanNames()); + log.debug(" MC: ConnectorStarted: {}", mc.isConnectorStarted()); + log.debug(" MC: CreateConnector: {}", mc.isCreateConnector()); + log.debug(" MC: CreateMBeanServer: {}", mc.isCreateMBeanServer()); + log.debug(" MC: FindTigerMbeanServer: {}", mc.isFindTigerMbeanServer()); + log.debug(" MC: UseMBeanServer: {}", mc.isUseMBeanServer()); + + log.debug(" MC->MBS: DefaultDomain: {}", mc.getMBeanServer().getDefaultDomain()); + log.debug(" MC->MBS: Domains: {}", (Object[])mc.getMBeanServer().getDomains()); + log.debug(" MC->MBS: MBeanCount: {}", mc.getMBeanServer().getMBeanCount()); + } catch (Exception ex) { + log.error(" MC: EXCEPTION: ", ex); + } + + // Set memory limit in order not to use too much memory + int memHeapPercent = properties.getUsage().getMemory().getJvmHeapPercentage(); + long memSize = properties.getUsage().getMemory().getSize(); + if (memHeapPercent > 0 || memSize > 0) { + final MemoryUsage memoryUsage = new MemoryUsage(); + if (memHeapPercent > 0) { + memoryUsage.setPercentOfJvmHeap(memHeapPercent); + log.debug("BrokerConfig: Limiting Broker Service instance memory usage to {}% of JVM heap size", memHeapPercent); + } else { + memoryUsage.setUsage(memSize); + log.debug("BrokerConfig: Limiting Broker Service instance memory usage to {} bytes", memSize); + } + final SystemUsage systemUsage = new SystemUsage(); + systemUsage.setMemoryUsage(memoryUsage); + brokerService.setSystemUsage(systemUsage); + } + + // start broker service instance + brokerService.start(); + + // register broker service interceptors + registerMessageInterceptors(brokerService); + + return brokerService; + } + + private void registerMessageInterceptors(BrokerService brokerService) { + // get message interceptor registry + final MessageInterceptorRegistry registry = MessageInterceptorRegistry.getInstance().get(brokerService); // or ...get(BrokerRegistry.getInstance().findFirst()); + log.trace("BrokerConfig: Message interceptor registry: {}", registry); + + if (properties.getMessageInterceptors()==null) { + log.warn("BrokerConfig: No message interceptors configured"); + return; + } + + log.debug("BrokerConfig: Message interceptors initializing..."); + List interceptorSpecs = properties.getMessageInterceptors() + .stream() + .map(c -> (BrokerCepProperties.MessageInterceptorSpec)c) + .collect(Collectors.toList()); + List interceptors = InterceptorHelper.newInstance() + .initializeInterceptors(registry, applicationContext, + properties.getMessageInterceptorsSpecs(), interceptorSpecs); + log.debug("BrokerConfig: Message interceptors initialized"); + + // register interceptors + log.debug("BrokerConfig: Registering message interceptors..."); + interceptors.forEach(i -> { + String destinationPattern = ((BrokerCepProperties.MessageInterceptorConfig) i.getInterceptorSpec()).getDestination(); + registry.addMessageInterceptorForTopic(destinationPattern, i); + log.debug("BrokerConfig: - Registered message interceptor with spec.: {}", i.getInterceptorSpec()); + }); + log.debug("BrokerConfig: Registering message interceptors... done"); + } + + private boolean isSecureUrl(String url) { + int p = url.indexOf(":"); + if (p<=0) return false; + String scheme = url.substring(0, p); + return scheme.startsWith("ssl") || scheme.contains("+ssl") || scheme.startsWith("https:"); + } + + private boolean secureConnectorsExist() { + if (properties.getBrokerUrlList()!=null) { + for (String url : properties.getBrokerUrlList()) + if (isSecureUrl(url.trim())) return true; + } + return false; + } + + private KeyManager[] readKeystore() throws Exception { + final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + final KeyStore keystore = KeyStore.getInstance(properties.getSsl().getKeystoreType()); + + //final Resource keystoreResource = new ClassPathResource( properties.getKeystoreFile() ); + final FileSystemResource keystoreResource = new FileSystemResource(properties.getSsl().getKeystoreFile()); + keystore.load(keystoreResource.getInputStream(), properties.getSsl().getKeystorePassword().toCharArray()); + keyManagerFactory.init(keystore, properties.getSsl().getKeystorePassword().toCharArray()); + final KeyManager[] keystoreManagers = keyManagerFactory.getKeyManagers(); + return keystoreManagers; + } + + private TrustManager[] readTruststore() throws Exception { + this.truststore = KeyStore.getInstance(properties.getSsl().getTruststoreType()); + + //final Resource truststoreResource = new ClassPathResource( properties.getTruststoreFile() ); + final FileSystemResource truststoreResource = new FileSystemResource(properties.getSsl().getTruststoreFile()); + this.truststore.load(truststoreResource.getInputStream(), properties.getSsl().getTruststorePassword().toCharArray()); + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(this.truststore); + final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + return trustManagers; + } + + public void writeTruststore() throws Exception { + //final Resource truststoreResource = new ClassPathResource( properties.getTruststoreFile() ); + final FileSystemResource truststoreResource = new FileSystemResource(properties.getSsl().getTruststoreFile()); + this.truststore.store(truststoreResource.getOutputStream(), properties.getSsl().getTruststorePassword().toCharArray()); + } + + /** + * Creates a new connection factory + */ + public ConnectionFactory connectionFactory() { + return connectionFactory(null); + } + + public ConnectionFactory connectionFactory(String brokerUrl) { + if (brokerUrl==null) brokerUrl = properties.getBrokerUrlForClients(); + + // Create connection factory based on Broker URL scheme + final ActiveMQConnectionFactory connectionFactory; + if (brokerUrl.startsWith("ssl")) { + log.debug("BrokerConfig: Creating new SSL connection factory instance: url={}", brokerUrl); + final ActiveMQSslConnectionFactory sslConnectionFactory = new ActiveMQSslConnectionFactory(brokerUrl); + try { + sslConnectionFactory.setTrustStore(properties.getSsl().getTruststoreFile()); + sslConnectionFactory.setTrustStoreType(properties.getSsl().getTruststoreType()); + sslConnectionFactory.setTrustStorePassword(properties.getSsl().getTruststorePassword()); + sslConnectionFactory.setKeyStore(properties.getSsl().getKeystoreFile()); + sslConnectionFactory.setKeyStoreType(properties.getSsl().getKeystoreType()); + sslConnectionFactory.setKeyStorePassword(properties.getSsl().getKeystorePassword()); + //sslConnectionFactory.setKeyStoreKeyPassword( properties.getSsl()........ ); + + connectionFactory = sslConnectionFactory; + } catch (final Exception theException) { + throw new Error(theException); + } + } else { + log.debug("BrokerConfig: Creating new non-SSL connection factory instance: url={}", brokerUrl); + connectionFactory = new ActiveMQConnectionFactory(brokerUrl); + } + + // Set credentials, if using local broker URL + if (brokerUrl.equals(properties.getBrokerUrlForClients()) && getBrokerLocalUserUsername()!=null) { + connectionFactory.setUserName(getBrokerLocalUserUsername()); + connectionFactory.setPassword(getBrokerLocalUserPassword()); + } + + // Other connection factory settings + //connectionFactory.setSendTimeout(....5000L); + //connectionFactory.setTrustedPackages(Arrays.asList("gr.iccs.imu.ems")); + connectionFactory.setTrustAllPackages(true); + connectionFactory.setWatchTopicAdvisories(true); + + // Make pooled connection factory + PooledConnectionFactory pooledConnectionFactory = new PooledConnectionFactory(connectionFactory); + pooledConnectionFactory.setMaxConnections(64); + log.trace("BrokerConfig: New connection factory created: {}", pooledConnectionFactory); + + return pooledConnectionFactory; + } + + public ConnectionFactory getConnectionFactoryFor(String connectionString) { + return connectionFactoryCache + .computeIfAbsent(connectionString, this::connectionFactory); + } + + public ConnectionFactory getConnectionFactoryForConsumer() { + String connStr; + if (StringUtils.isNotBlank(properties.getBrokerUrlForConsumer())) { + log.debug("BrokerConfig.getConnectionFactoryForConsumer(): Broker URL for Broker-CEP consumer instance: {}", properties.getBrokerUrlForConsumer()); + connStr = properties.getBrokerUrlForConsumer(); + } else { + log.debug("BrokerConfig.getConnectionFactoryForConsumer(): Default broker URL will be used for Broker-CEP consumer instance: {}", properties.getBrokerUrlForClients()); + connStr = null; + } + return connectionFactoryCache + .computeIfAbsent(connStr, this::connectionFactory); + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/InterceptorHelper.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/InterceptorHelper.java new file mode 100644 index 0000000..f15995e --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/InterceptorHelper.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker; + +import gr.iccs.imu.ems.brokercep.broker.interceptor.AbstractMessageInterceptor; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.inteceptor.MessageInterceptorRegistry; +import org.springframework.context.ApplicationContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Slf4j +public class InterceptorHelper { + public static InterceptorHelper newInstance() { + return new InterceptorHelper(); + } + + public List initializeInterceptors(final MessageInterceptorRegistry registry, + ApplicationContext applicationContext, + Map specs, + List interceptorSpecs) + { + log.debug("InterceptorHelper: Initialize interceptors..."); + + List interceptors = new ArrayList<>(); + interceptorSpecs + .forEach(spec -> { + AbstractMessageInterceptor interceptor = initializeInterceptor(registry, applicationContext, specs, spec); + interceptors.add(interceptor); + }); + log.debug("InterceptorHelper: Initialize interceptors... done"); + return interceptors; + } + + @SuppressWarnings("unchecked") + public AbstractMessageInterceptor initializeInterceptor(final MessageInterceptorRegistry registry, + ApplicationContext applicationContext, + Map specs, + BrokerCepProperties.MessageInterceptorSpec interceptorSpec) + { + log.debug("InterceptorHelper: Initializing message interceptor with spec.: {}", interceptorSpec); + String interceptorClassName = interceptorSpec.getClassName(); + Class interceptorClass; + try { + interceptorClass = (Class) Class.forName(interceptorClassName); + } catch (ClassNotFoundException e) { + log.error("InterceptorHelper: Error while registering message interceptor: {}. Exception: ", interceptorSpec, e); + throw new RuntimeException(e); + } + + AbstractMessageInterceptor interceptor = null; + Exception lastException = null; + // Try 2-args constructor + try { + interceptor = interceptorClass + .getDeclaredConstructor(MessageInterceptorRegistry.class, ApplicationContext.class) + .newInstance(registry, applicationContext); + } catch (Exception e) { + log.debug("InterceptorHelper: Instantiating message interceptor with 2-args constructor failed: {} {}", + e.getClass().getSimpleName(), e.getMessage()); + lastException = e; + } + // Try 1-arg constructor + if (interceptor==null) { + try { + interceptor = interceptorClass + .getDeclaredConstructor(MessageInterceptorRegistry.class) + .newInstance(registry); + } catch (Exception e) { + log.debug("InterceptorHelper: Instantiating message interceptor with 1-arg constructor failed: {} {}", + e.getClass().getSimpleName(), e.getMessage()); + lastException = e; + } + } + // Try no-args constructor + if (interceptor==null) { + try { + interceptor = interceptorClass + .getDeclaredConstructor() + .newInstance(); + } catch (Exception e) { + log.debug("InterceptorHelper: Instantiating message interceptor with no-args constructor failed: {} {}", + e.getClass().getSimpleName(), e.getMessage()); + lastException = e; + } + } + // Throw exception if all tries failed + if (interceptor==null) { + log.error("InterceptorHelper: Instantiating message interceptor failed: Last exception: ", lastException); + throw new RuntimeException("Interceptor initialization exception", lastException); + } + + // Initialize interceptor + interceptor.setRegistry(registry); + interceptor.setApplicationContext(applicationContext); + interceptor.setMessageInterceptorSpecs(specs); + interceptor.setInterceptorSpec(interceptorSpec); + interceptor.initialized(); + log.debug("InterceptorHelper: Message interceptor initialized: {}", interceptorSpec); + + return interceptor; + } + + public AbstractMessageInterceptor initializeInterceptor(final AbstractMessageInterceptor parent, + BrokerCepProperties.MessageInterceptorSpec interceptorSpec) + { + return initializeInterceptor(parent.getRegistry(), parent.getApplicationContext(), + parent.getMessageInterceptorSpecs(), interceptorSpec); + } + + public AbstractMessageInterceptor initializeInterceptorFor(final AbstractMessageInterceptor parent, String specId) + { + BrokerCepProperties.MessageInterceptorSpec interceptorSpec = + getInterceptorSpecFor(specId, parent.getMessageInterceptorSpecs()); + return initializeInterceptor(parent.getRegistry(), parent.getApplicationContext(), + parent.getMessageInterceptorSpecs(), interceptorSpec); + } + + /*public AbstractMessageInterceptor initializeInterceptorFor(final MessageInterceptorRegistry registry, + ApplicationContext applicationContext, + Map specs, + String specId) + { + BrokerCepProperties.MessageInterceptorSpec interceptorSpec = getInterceptorSpecFor(specId, specs); + return initializeInterceptor(registry, applicationContext, specs, interceptorSpec); + }*/ + + public BrokerCepProperties.MessageInterceptorSpec getInterceptorSpecFor(String specId, Map specs) { + log.debug("InterceptorHelper: getInterceptorSpecFor: spec-id={}, specs={}", specId, specs); + BrokerCepProperties.MessageInterceptorSpec spec; + if (specId.startsWith("#")) { + specId = specId.substring(1); + spec = specs.get(specId); + if (spec==null) + throw new IllegalArgumentException("Message Interceptor Spec Id not found in configuration: "+specId); + } else { + spec = new BrokerCepProperties.MessageInterceptorSpec(); + spec.setClassName(specId); + } + return spec; + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/SimpleBrokerAuthorizationPlugin.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/SimpleBrokerAuthorizationPlugin.java new file mode 100644 index 0000000..f35b9e3 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/SimpleBrokerAuthorizationPlugin.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker; + +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.Broker; +import org.apache.activemq.broker.BrokerPlugin; +import org.apache.activemq.filter.DestinationMapEntry; +import org.apache.activemq.security.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple AMQ broker authorization plugin + */ +@Slf4j +public class SimpleBrokerAuthorizationPlugin implements BrokerPlugin { + public final static String ADMIN_GROUP = "admins"; + public final static String RW_USER_GROUP = "users_RW"; + public final static String RO_USER_GROUP = "users_RO"; + public final static String ALL_GROUPS = RO_USER_GROUP+","+RW_USER_GROUP+","+ADMIN_GROUP; + public final static String RWUSER_ADMIN_GROUPS = RW_USER_GROUP+","+ADMIN_GROUP; + + private AuthorizationMap map; + + public SimpleBrokerAuthorizationPlugin() { + _prepareAuthorizationMap(); + } + + public Broker installPlugin(Broker broker) { + if (map == null) { + throw new IllegalArgumentException("You must configure a 'map' property"); + } + return new AuthorizationBroker(broker, map); + } + + public AuthorizationMap getMap() { + return map; + } + + public void setMap(AuthorizationMap map) { + this.map = map; + } + + private void _prepareAuthorizationMap() { + try { + // prepare authorization entry for 'ActiveMQ.Advisory' topics + AuthorizationEntry mapEntry1 = new AuthorizationEntry(); + mapEntry1.setTopic("ActiveMQ.Advisory.>"); + mapEntry1.setRead(ALL_GROUPS); + mapEntry1.setWrite(ALL_GROUPS); + mapEntry1.setAdmin(ALL_GROUPS); + + // prepare authorization entry for all topics + AuthorizationEntry mapEntry = new AuthorizationEntry(); + mapEntry.setTopic(">"); + mapEntry.setRead(ALL_GROUPS); + mapEntry.setWrite(RWUSER_ADMIN_GROUPS); + mapEntry.setAdmin(ADMIN_GROUP); + + // prepare authorization map entries + List mapEntries = new ArrayList<>(); + mapEntries.add(mapEntry1); + mapEntries.add(mapEntry); + + // prepare authorization map + DefaultAuthorizationMap defaultAuthorizationMap = new DefaultAuthorizationMap(); + defaultAuthorizationMap.setAuthorizationEntries(mapEntries); + + map = defaultAuthorizationMap; + } catch (Exception ex) { + log.error("BrokerConfig.SimpleAuthorizationPlugin._updateAuthorizationBroker(): EXCEPTION: ", ex); + throw new RuntimeException(ex); + } + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/AbstractMessageInterceptor.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/AbstractMessageInterceptor.java new file mode 100644 index 0000000..66b70ce --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/AbstractMessageInterceptor.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker.interceptor; + +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.ProducerBrokerExchange; +import org.apache.activemq.broker.inteceptor.MessageInterceptor; +import org.apache.activemq.broker.inteceptor.MessageInterceptorRegistry; +import org.apache.activemq.command.Message; +import org.springframework.context.ApplicationContext; + +import java.util.Map; + +@Slf4j +@Data +public abstract class AbstractMessageInterceptor implements MessageInterceptor { + protected MessageInterceptorRegistry registry; + protected ApplicationContext applicationContext; + protected Map messageInterceptorSpecs; + + protected BrokerCepProperties.MessageInterceptorSpec interceptorSpec; + private ProducerBrokerExchange producerBrokerExchange; + + public void initialized() { } + + @Override + public void intercept(ProducerBrokerExchange producerBrokerExchange, Message message) { + try { + this.producerBrokerExchange = producerBrokerExchange; + intercept(message); + registry.injectMessage(producerBrokerExchange, message); + } catch (Exception e) { + log.error("AbstractMessageInterceptor: EXCEPTION: ", e); + } + } + + public abstract void intercept(Message message); +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/CompositeInterceptor.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/CompositeInterceptor.java new file mode 100644 index 0000000..89acd59 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/CompositeInterceptor.java @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker.interceptor; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class CompositeInterceptor extends AbstractMessageInterceptor { +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/LogMessageUpdateInterceptor.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/LogMessageUpdateInterceptor.java new file mode 100644 index 0000000..f2aa1b1 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/LogMessageUpdateInterceptor.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker.interceptor; + +import gr.iccs.imu.ems.brokercep.broker.BrokerConfig; +import gr.iccs.imu.ems.brokercep.event.EventRecorder; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.command.ActiveMQMessage; +import org.apache.activemq.command.Message; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Slf4j +@Lazy +@Component +public class LogMessageUpdateInterceptor extends AbstractMessageInterceptor { + private EventRecorder eventRecorder; + + @Override + public void initialized() { + this.eventRecorder = applicationContext.getBean(BrokerConfig.class).getEventRecorder(); + log.debug("LogMessageUpdateInterceptor: Enabled: {}", eventRecorder!=null); + eventRecorder.startRecording(); + } + + @Override + public void intercept(Message message) { + try { + if (eventRecorder!=null && message instanceof ActiveMQMessage) + eventRecorder.recordEvent((ActiveMQMessage)message); + } catch (Exception e) { + log.error("LogMessageUpdateInterceptor: EXCEPTION: ", e); + } + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/MessageForwarderInterceptor.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/MessageForwarderInterceptor.java new file mode 100644 index 0000000..07321f9 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/MessageForwarderInterceptor.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker.interceptor; + +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.command.ActiveMQObjectMessage; +import org.apache.activemq.command.ActiveMQTextMessage; +import org.apache.activemq.command.Message; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationContext; + +import javax.jms.JMSException; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +@Slf4j +public class MessageForwarderInterceptor extends AbstractMessageInterceptor { + private final static MessageQueueProcessor messageQueueProcessor = new MessageQueueProcessor(); + + public void initialized() { + startQueueProcessing(applicationContext); + } + + @Override + public void intercept(Message message) { + log.trace("MessageForwarderInterceptor: Message: {}", message); + // enqueue message for processing + messageQueueProcessor.getMessageQueue().add(message); + } + + private void startQueueProcessing(ApplicationContext applicationContext) { + synchronized (messageQueueProcessor) { + if (!messageQueueProcessor.isRunning()) { + messageQueueProcessor.setApplicationContext(applicationContext); + messageQueueProcessor.start(); + } + } + } + + protected static class MessageQueueProcessor implements Runnable { + private final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + private ApplicationContext applicationContext; + private BrokerCepService brokerCepService; + private Thread runner; + private boolean keepRunning; + protected List forwardDestinations; + + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public Queue getMessageQueue() { + return messageQueue; + } + + @Override + public void run() { + log.info("MessageQueueProcessor: Starts processing message queue and forward messages"); + while (keepRunning) { + String connectionString = null; + String username = null; + String password = null; + String destination = null; + try { + Message m = messageQueue.take(); + log.trace("MessageQueueProcessor: Message taken from queue: {}", m); + + if (! isMessageForwardPossible(m)) { + //keepRunning = false; + continue; + } + + destination = m.getDestination().getPhysicalName(); + EventMap eventMap = messageToEvent(m); + for (BrokerCepProperties.ForwardDestinationConfig config : forwardDestinations) { + connectionString = config.getConnectionString(); + username = config.getUsername(); + password = config.getPassword(); + log.trace("MessageQueueProcessor: Forwarding message to: {}/{} (username: {}): {}", + connectionString, destination, username, eventMap); + if (StringUtils.isBlank(username)) + brokerCepService.publishEvent(connectionString, destination, eventMap); + else + brokerCepService.publishEvent(connectionString, username, password, destination, eventMap); + log.debug("MessageQueueProcessor: Message forwarded to: {}/{} (username: {}): {}", + connectionString, destination, username, eventMap); + } + + } catch (InterruptedException e) { + log.error("MessageQueueProcessor: Exception while taking message from queue: ", e); + } catch (JMSException e) { + log.error("MessageQueueProcessor: Exception while sending message to: {}/{}: Exception: ", + connectionString, destination, e); + } + } + runner = null; + log.warn("MessageQueueProcessor: Stopped processing message queue"); + } + + private boolean isMessageForwardPossible(Message m) { + if (brokerCepService==null) { + try { + this.brokerCepService = applicationContext.getBean(BrokerCepService.class); + if (brokerCepService==null) { + log.error("MessageQueueProcessor: Null BrokerCepService instance returned"); + return false; + } + } catch (Exception e) { + log.error("MessageQueueProcessor: Exception while getting BrokerCepService instance: ", e); + return false; + } + } + if (forwardDestinations==null) { + try { + BrokerCepProperties bcp = applicationContext.getBean(BrokerCepProperties.class); + if (bcp==null) { + log.error("MessageQueueProcessor: Null BrokerCepProperties instance returned"); + return false; + } + forwardDestinations = bcp.getMessageForwardDestinations(); + log.info("MessageQueueProcessor: Forward destinations initialized: {}", forwardDestinations); + } catch (Exception e) { + log.error("MessageQueueProcessor: Exception while getting BrokerCepProperties instance: ", e); + return false; + } + } + if (forwardDestinations.size()==0) { + log.debug("MessageQueueProcessor: No forward destinations specified. Discarding message: {}", m); + return false; + } + return true; + } + + private EventMap messageToEvent(Message message) { + try { + log.trace("MessageForwarderInterceptor.messageToEvent(): message: {}", message); + Map eventProperties = message.getProperties(); + log.trace("MessageForwarderInterceptor.messageToEvent(): event-properties: {}", eventProperties); + if (message instanceof ActiveMQObjectMessage mesg) { + if (mesg.getObject() instanceof Map) { + EventMap eventMap = new EventMap(StrUtil.castToMapStringObject(mesg.getObject())); + if (eventProperties!=null) eventMap.putAll(eventProperties); + log.trace("MessageForwarderInterceptor.messageToEvent(): event-map: {}", eventMap); + return eventMap; + } + } else if (message instanceof ActiveMQTextMessage mesg) { + // Send message to Esper + EventMap eventMap = EventMap.parseEventMap(mesg.getText()); + if (eventProperties!=null) eventMap.putAll(eventProperties); + log.trace("MessageForwarderInterceptor.messageToEvent(): event-map: {}", eventMap); + return eventMap; + } else { + log.warn("MessageForwarderInterceptor.messageToEvent(): Message ignored: type={}", message.getClass().getName()); + } + } catch (Exception ex) { + log.error("MessageForwarderInterceptor.messageToEvent(): EXCEPTION: ", ex); + } + throw new RuntimeException("Unsupported Message type: "+message.getClass()); + } + + public synchronized void start() { + if (runner==null) { + keepRunning = true; + runner = new Thread(this); + runner.setDaemon(true); + runner.start(); + } else { + log.warn("MessageQueueProcessor is already running"); + } + } + + public synchronized void stop() { + if (isRunning()) { + keepRunning = false; + runner.interrupt(); + } else { + log.warn("MessageQueueProcessor is not running"); + } + } + + public boolean isRunning() { + if (runner==null) return false; + return runner.isAlive(); + } + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/NodePropertiesMessageUpdateInterceptor.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/NodePropertiesMessageUpdateInterceptor.java new file mode 100644 index 0000000..d17fbc7 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/NodePropertiesMessageUpdateInterceptor.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker.interceptor; + +import gr.iccs.imu.ems.brokercep.properties.NodeProperties; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.command.Message; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Lazy +@Component +public class NodePropertiesMessageUpdateInterceptor extends AbstractMessageInterceptor { + private NodeProperties nodeProperties; + + @Override + public void initialized() { + this.nodeProperties = applicationContext.getBean(NodeProperties.class); + log.debug("NodePropertiesMessageUpdateInterceptor: Node properties: {}", nodeProperties); + assert (nodeProperties != null); + } + + @Override + public void intercept(Message message) { + // Check if interceptor is enabled + if (! nodeProperties.isAddNodePropertiesToEventsEnabled()) { + log.trace("NodePropertiesMessageUpdateInterceptor: Not enabled!"); + return; + } + + log.trace("NodePropertiesMessageUpdateInterceptor: Message: {}", message); + try { + // Check if node properties have already been set. + // If at least one node property is set then we skip further processing. + if (message.getProperties()!=null) { + if (nodeProperties.getNodeProperties().keySet().stream().anyMatch(message.getProperties()::containsKey)) { + log.trace("NodePropertiesMessageUpdateInterceptor: Found at least one node property set in message. Skipping further processing: message: {}", message); + return; + } + } + + // Add node properties as message properties + log.debug("NodePropertiesMessageUpdateInterceptor: Message properties before adding node properties: properties={}, message: {}", message.getProperties(), message); + + for (Map.Entry entry : nodeProperties.getNodeProperties().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (StringUtils.isBlank(key)) continue; + if (nodeProperties.isSkipNullValues() && value==null || nodeProperties.isSkipBlankValues() && StringUtils.isBlank(value)) { + log.trace("NodePropertiesMessageUpdateInterceptor: Skipping null- or blank-value node property due to configuration: property={}, message: {}", key, message); + continue; + } + log.trace("NodePropertiesMessageUpdateInterceptor: Added node property to message: property={}, value={}, message: {}", key, value, message); + message.setProperty(key, value); + } + log.debug("NodePropertiesMessageUpdateInterceptor: Message properties after adding node properties: properties={}, message: {}", message.getProperties(), message); + + } catch (Exception e) { + log.error("NodePropertiesMessageUpdateInterceptor: EXCEPTION: ", e); + } + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/SequentialCompositeInterceptor.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/SequentialCompositeInterceptor.java new file mode 100644 index 0000000..423fac1 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/SequentialCompositeInterceptor.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker.interceptor; + +import gr.iccs.imu.ems.brokercep.broker.InterceptorHelper; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.command.Message; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class SequentialCompositeInterceptor extends CompositeInterceptor { + private final List interceptors = new ArrayList<>(); + + @Override + public void initialized() { + initChildInterceptors(); + } + + private void initChildInterceptors() { + log.debug("SequentialCompositeInterceptor: Initializing child interceptors..."); + InterceptorHelper helper = InterceptorHelper.newInstance(); + List params = getInterceptorSpec().getParams(); + if (params!=null) + params.forEach(p -> { + log.debug(" - SequentialCompositeInterceptor: Initializing child interceptor for: {}", p); + addMessageInterceptor(helper.initializeInterceptorFor(this, p)); + log.debug(" - SequentialCompositeInterceptor: Child interceptor initialized for: {}", p); + }); + log.debug("SequentialCompositeInterceptor: Initializing child interceptors...done"); + } + + public void addMessageInterceptor(AbstractMessageInterceptor interceptor) { + if (interceptor == null) throw new IllegalArgumentException("Argument is null"); + interceptors.add(interceptor); + } + + @Override + public void intercept(Message message) { + log.debug("SequentialCompositeInterceptor: Message IN: {}", message); + interceptors.forEach(interceptor -> { + log.debug("SequentialCompositeInterceptor: - Calling interceptor: {}", interceptor.getClass().getSimpleName()); + interceptor.setProducerBrokerExchange(getProducerBrokerExchange()); + interceptor.intercept(message); + }); + log.debug("SequentialCompositeInterceptor: Message OUT: {}", message); + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/SourceAddressMessageUpdateInterceptor.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/SourceAddressMessageUpdateInterceptor.java new file mode 100644 index 0000000..0005d12 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/broker/interceptor/SourceAddressMessageUpdateInterceptor.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.broker.interceptor; + +import gr.iccs.imu.ems.util.EmsConstant; +import gr.iccs.imu.ems.util.NetUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.Connection; +import org.apache.activemq.command.Message; +import org.apache.commons.lang3.StringUtils; + +@Slf4j +public class SourceAddressMessageUpdateInterceptor extends AbstractMessageInterceptor { + private final String SOURCE_ADDRESS_PROPERTY_NAME = EmsConstant.EVENT_PROPERTY_SOURCE_ADDRESS; + + @Override + public void intercept(Message message) { + log.trace("SourceAddressMessageUpdateInterceptor: Message: {}", message); + try { + Object sourceProperty = message.getProperty(SOURCE_ADDRESS_PROPERTY_NAME); + if (sourceProperty!=null && StringUtils.isNotBlank(sourceProperty.toString())) { + log.trace("SourceAddressMessageUpdateInterceptor: Message has Producer Host property set: {}", sourceProperty); + return; + } + + // get remote address from connection + Connection conn = getProducerBrokerExchange().getConnectionContext().getConnection(); + log.trace("SourceAddressMessageUpdateInterceptor: Connection: {}", conn); + String address = conn.getRemoteAddress(); + log.trace("SourceAddressMessageUpdateInterceptor: Producer address: {}", address); + + // extract remote host address + if (StringUtils.isNotBlank(address)) { + address = StringUtils.substringsBetween(address, "//", ":") [0]; + } + log.trace("SourceAddressMessageUpdateInterceptor: Producer host: {}", address); + + // check if host address is local + boolean isLocal = StringUtils.isBlank(address) || NetUtil.isLocalAddress(address.trim()); + if (isLocal) { + log.trace("SourceAddressMessageUpdateInterceptor: Producer host is local. Getting our public IP address"); + address = NetUtil.getPublicIpAddress(); + log.trace("SourceAddressMessageUpdateInterceptor: Producer host (public): {}", address); + } else { + log.trace("SourceAddressMessageUpdateInterceptor: Producer host is not local. Ok"); + } + + // get message remote address old value (if any) + String oldAddress = (String) message.getProperty(SOURCE_ADDRESS_PROPERTY_NAME); + log.trace("SourceAddressMessageUpdateInterceptor: Producer host property in message: {}", oldAddress); + + // set new remote address value, if needed + if (StringUtils.isBlank(oldAddress) && StringUtils.isNotBlank(address)) { + log.trace("SourceAddressMessageUpdateInterceptor: Setting producer host property in message: host={}, message={}", address, message); + message.setProperty(SOURCE_ADDRESS_PROPERTY_NAME, address); + log.debug("SourceAddressMessageUpdateInterceptor: Set producer host property in message: host={}, message={}", address, message); + } else if (StringUtils.isNotBlank(oldAddress)) { + log.debug("SourceAddressMessageUpdateInterceptor: Producer host property already set (keeping previous value): host={}, message={}", oldAddress, message); + } else if (StringUtils.isBlank(address)) { + log.warn("SourceAddressMessageUpdateInterceptor: Could not resolve Producer host property: message={}", message); + } + + } catch (Exception e) { + log.error("SourceAddressMessageUpdateInterceptor: EXCEPTION: ", e); + } + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepEvalAggregator.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepEvalAggregator.java new file mode 100644 index 0000000..debb807 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepEvalAggregator.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.cep; + +import com.espertech.esper.collection.Pair; +import com.espertech.esper.epl.agg.aggregator.AggregationMethod; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class CepEvalAggregator implements AggregationMethod { + private final LinkedHashMap entries = new LinkedHashMap<>(); + + public void clear() { + log.debug("CepEvalAggregator.clear(): aggregator-hash={}", hashCode()); + entries.clear(); + } + + public void enter(Object value) { + log.debug("CepEvalAggregator.enter(): aggregator-hash={}, input={}, hash={}", hashCode(), value, value.hashCode()); + traceValue("ENTER-BEFORE", value, null, true); + if (value instanceof Object[]) + entries.put(Arrays.hashCode((Object[]) value), (Object[]) value); // 0:formula, 1:stream-names, 2+:EventMap + else + log.error("CepEvalAggregator.enter(): ERROR: WRONG ARG TYPE: Expected Object[]: aggregator-hash={}, input={}, input-type={}", hashCode(), value, value.getClass().getName()); + traceValue("ENTER-AFTER", value, null, true); + } + + public void leave(Object value) { + log.debug("CepEvalAggregator.leave(): aggregator-hash={}, input={}, hash={}", hashCode(), value, value.hashCode()); + traceValue("LEAVE-BEFORE", value, null, true); + +// int p = findEntry(value); +// Object[] removedObject = p!=-1 ? entries.remove(p) : null; + int valueHash = Arrays.hashCode((Object[]) value); + Object[] removedObject = entries.remove(valueHash); + log.debug("CepEvalAggregator.leave(): aggregator-hash={}, input={}, hash={}, p={}, removed={}", hashCode(), value, value.hashCode(), + /*p*/null, removedObject==null ? null : Arrays.asList(removedObject)); + + traceValue("LEAVE-AFTER", removedObject, value, true); + } + + /*private int findEntry(Object value) { + log.trace("CepEvalAggregator.findEntry: BEGIN: to-remove={}", value); + if (value==null) { + log.trace("CepEvalAggregator.findEntry: END: ILLEGAL ARG: NULL ARG: to-remove={}", value); + return -1; + } + if (log.isTraceEnabled()) + log.trace("CepEvalAggregator.findEntry: to-remove-class: {}", value.getClass().getName()); + if (! (value instanceof Object[])) { + log.trace("CepEvalAggregator.findEntry: END: ILLEGAL ARG: Not an Object[]: class={}, to-remove={}", value.getClass().getName(), value); + return -2; + } + Object[] valArr = (Object[]) value; + int valArrLen = valArr.length; + if (log.isTraceEnabled()) + log.trace("CepEvalAggregator.findEntry: to-remove: size={}, str={}", Arrays.toString(valArr), valArrLen); + + log.trace("CepEvalAggregator.findEntry: num-of-entries: {}", entries.size()); + int pos = -1; + for (Object[] oArr : entries.values()) { + pos++; + log.trace("CepEvalAggregator.findEntry: entry-item: pos={}, item={}, to-remove={}", pos, oArr, value); + if (oArr==value) { + log.trace("CepEvalAggregator.findEntry: END: FOUND-SAME-OBJECT: pos={}, to-remove={}", pos, value); + return pos; + } + log.trace("CepEvalAggregator.findEntry: entry-item: pos={}, item-arr-len={}, to-remove-arr-len={}", pos, oArr.length, valArrLen); + if (oArr.length!=valArrLen) + continue; +// int x = _findEntry_extraChecks(valArr, oArr); +// log.trace("CepEvalAggregator.findEntry: entry-item: pos={}, extra-checks-result={}", pos, x); +// if (x != 0) +// continue; + if (log.isTraceEnabled()) + log.trace("CepEvalAggregator.findEntry: entry-item: pos={}, item-arr-hash={}, to-remove-arr-hash={}", pos, Arrays.hashCode(oArr), Arrays.hashCode(valArr)); + if (Arrays.hashCode(oArr) != Arrays.hashCode(valArr)) + continue; + log.trace("CepEvalAggregator.findEntry: END: FOUND-SAME-ARRAY-HASH: pos={}, to-remove={}", pos, value); + return pos; + } + + log.trace("CepEvalAggregator.findEntry: END: NOT FOUND: {}", value); + return -10; + } + + private static Integer _findEntry_extraChecks(Object[] valArr, Object[] oArr) { + for (int i = 0; i< oArr.length; i++) { + if (i>=2) { + if (oArr[i] instanceof EventMap && valArr[i] instanceof EventMap) { + if (((EventMap) oArr[i]).getEventId() != ((EventMap) valArr[i]).getEventId()) + return -4; + } else + if (oArr[i] instanceof Pair && valArr[i] instanceof Pair) { + log.trace("CepEvalAggregator._findEntry_extraChecks: oArr[{}]: {} / {}", i, oArr[i].getClass().getName(), oArr[i]); + log.trace("CepEvalAggregator._findEntry_extraChecks: valArr[{}]: {} / {}", i, valArr[i].getClass().getName(), valArr[i]); + Object e1 = ((Pair) oArr[i]).getFirst(); + Object e2 = ((Pair) valArr[i]).getFirst(); + log.trace("CepEvalAggregator._findEntry_extraChecks: e1: {} / {}", e1.getClass().getName(), e1); + log.trace("CepEvalAggregator._findEntry_extraChecks: e2: {} / {}", e2.getClass().getName(), e2); + if (e1 instanceof EventMap && e2 instanceof EventMap) { + log.trace("CepEvalAggregator._findEntry_extraChecks: e1 + e2 ARE EventMaps"); + log.trace("CepEvalAggregator._findEntry_extraChecks: e1-id: {}", ((EventMap) e1).getEventId()); + log.trace("CepEvalAggregator._findEntry_extraChecks: e2-id: {}", ((EventMap) e2).getEventId()); + if (((EventMap) e1).getEventId() != ((EventMap) e2).getEventId()) + return -5; + log.trace("CepEvalAggregator._findEntry_extraChecks: e1-id AND e2-id MATCH!!!"); + } else { + log.trace("CepEvalAggregator._findEntry_extraChecks: e1 + e2 ARE *NOT* EventMaps"); + return -6; + } + } else + { + return -7; + } + } + if (oArr[i].hashCode()!= valArr[i].hashCode()) + return -8; + } + return 0; + }*/ + + public Object getValue() { + log.debug("CepEvalAggregator.getValue(): BEGIN"); + + // Get an unmodifiable local copy of entries + Map _entries; + synchronized (entries) { +// _entries = Collections.unmodifiableList(entries); + _entries = Collections.unmodifiableMap(entries); + } + + if (_entries.size() == 0) { + log.debug("CepEvalAggregator.getValue(): END_0: aggregator-hash={}, result=0", hashCode()); + return 0; + } + + // get formula and stream names (they must be identical for all entries) +// Object[] first = _entries.get(0); + Object[] first = _entries.values().iterator().next(); + String formula = (String) first[0]; + String[] streamNames = ((String) first[1]).split(","); + + // initialize event lists for each stream + List> lists = new ArrayList<>(); + for (int i = 0; i < streamNames.length; i++) { + lists.add(new ArrayList<>()); + } + + // append events from entries into stream event lists + for (Object[] entry : _entries.values()) { + if (!entry[0].equals(formula) && !entry[1].equals(streamNames)) + throw new IllegalArgumentException("Aggregator entries do not contain the same formula or stream names in arguments #0 or #1"); + for (int i = 0; i < streamNames.length; i++) { + Object currentEntry = entry[i + 2]; + + // If entry is a Pair then extract first value (must be an EventMap or Map) + if (currentEntry instanceof Pair) { + Pair pair = (Pair)currentEntry; + Object firstInPair = ((Pair)currentEntry).getFirst(); + log.trace("CepEvalAggregator.getValue(): First: {} -- {}", pair.getFirst().getClass().getName(), pair.getFirst()); + log.trace("CepEvalAggregator.getValue(): Second: {} -- {}", pair.getSecond().getClass().getName(), pair.getSecond()); + if (firstInPair instanceof HashMap) + currentEntry = firstInPair; + } + + // Process entry + if (EventMap.class.isAssignableFrom(currentEntry.getClass())) { + lists.get(i).add((EventMap) currentEntry); + } else if (HashMap.class.isAssignableFrom(currentEntry.getClass())) { + EventMap eventMap = new EventMap(StrUtil.castToMapStringObject(currentEntry)); + lists.get(i).add(eventMap); + } else { + log.error("CepEvalAggregator.getValue(): ERROR: Event type is not supported: {}, Event:\n{}", + currentEntry.getClass().getName(), currentEntry); + throw new RuntimeException("Event type is not supported: " + currentEntry.getClass().getName()); + } + } + } + + // extract values from events + log.debug("CepEvalAggregator.getValue(): formula: {}", formula); + log.debug("CepEvalAggregator.getValue(): streams: {}", java.util.Arrays.asList(streamNames)); + log.debug("CepEvalAggregator.getValue(): stream-event-lists: {}", lists.size()); + List> dataLists = new ArrayList<>(); + for (int i = 0, n = lists.size(); i < n; i++) { + log.trace("CepEvalAggregator.getValue(): event-list-{}: {}", i, lists.get(i)); + //List data = lists.get(i).stream().map(event -> (Double) event.get("metricValue")).collect(Collectors.toList()); + List data = lists.get(i).stream().map(EventMap::getMetricValue).collect(Collectors.toList()); + log.trace("CepEvalAggregator.getValue(): data-list-{}: {}", i, data); + dataLists.add(data); + } + + // prepare arguments of MathParser + Map> args = new HashMap<>(); + for (int i = 0; i < streamNames.length; i++) { + args.put(streamNames[i].trim(), dataLists.get(i)); + } + log.debug("CepEvalAggregator.getValue(): stream-data-lists: {}", args); + + // use MathParser to evaluate formula using stream data lists + double result = MathUtil.evalAgg(formula, args); + log.debug("CepEvalAggregator.getValue(): END: aggregator-hash={}, result={}", hashCode(), result); + return result; + } + + + private void traceValue(String logPrefix, Object value, Object match, boolean listEntries) { + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: BEGIN: {}", logPrefix, value); + if (value==null) return; + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: CLASS: {}", logPrefix, value.getClass().getName()); + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: HASH: {}", logPrefix, value.hashCode()); + if (! (value instanceof Object[])) return; + Object[] oArr = (Object[])value; + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: SIZE: {}", logPrefix, oArr.length); + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: A-HASH: {}", logPrefix, Arrays.hashCode(oArr)); + int i=0; + for (Object oVal : oArr) { + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: ,,,,,,,,,,,,,,,,,,,,,,,,,,,", logPrefix); + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: ITEM: {}", logPrefix, i++); + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: VAL: {}", logPrefix, oVal); + if (oVal==null) continue; + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: CLASS: {}", logPrefix, oVal.getClass().getName()); + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: HASH: {}", logPrefix, oVal.hashCode()); + EventMap event = null; + if (oVal instanceof Pair) { + Pair p = (Pair)oVal; + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: 1-ST: {}", logPrefix, p.getFirst()); + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: 2-ND: {}", logPrefix, p.getSecond()); + traceValue(logPrefix+"-PAIR-1ST", p.getFirst(), null, false); + traceValue(logPrefix+"-PAIR-2ND", p.getSecond(), null, false); + if (p.getFirst() instanceof EventMap) event = (EventMap) p.getFirst(); + } + else if (oVal instanceof EventMap) event = (EventMap) oVal; + if (event!=null) + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: E-ID: {}", logPrefix, event.getEventId()); + else + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: NO EVENT: {}", logPrefix, oVal.getClass().getName()); + + if (match==null) continue; + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: MATCH-1: {}", logPrefix, oVal==match); + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: MATCH-2: {} =? {}", logPrefix, oVal.hashCode(), match.hashCode()); + } + log.trace("CepEvalAggregator.logValue: LOG-VALUE: {}: ,,,,,,,,,,,,,,,,,,,,,,,,,,,", logPrefix); + + if (listEntries) { + log.trace("CepEvalAggregator.logValue: LIST-ENTRIES: ----> ENTRIES: {}", entries.size()); + int j = 0; + for (Object arr : entries.values()) { + log.trace("CepEvalAggregator.logValue: LIST-ENTRIES: ----> ENTRY-{}: ------------------------------", j); + traceValue("LOG-VALUE-"+j, arr, null, false); + } + log.trace("CepEvalAggregator.logValue: LIST-ENTRIES: ----> ENTRY-END: ------------------------------"); + } + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepEvalAggregatorFactory.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepEvalAggregatorFactory.java new file mode 100644 index 0000000..a42d4ea --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepEvalAggregatorFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.cep; + +import com.espertech.esper.client.hook.AggregationFunctionFactory; +import com.espertech.esper.collection.Pair; +import com.espertech.esper.epl.agg.aggregator.AggregationMethod; +import com.espertech.esper.epl.agg.service.common.AggregationValidationContext; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.Map; + +@Slf4j +public class CepEvalAggregatorFactory implements AggregationFunctionFactory { + + private String aggregatorFunctionName; + + public Class getValueType() { + return Double.class; + } + + public AggregationMethod newAggregator() { + return new CepEvalAggregator(); + } + + public void setFunctionName(String functionName) { + aggregatorFunctionName = functionName; + } + + public void validate(AggregationValidationContext validationContext) { + log.debug("CepEvalAggregatorFactory.validate(): BEGIN: validationContext: {}", validationContext); + Class[] paramType = validationContext.getParameterTypes(); + log.debug("CepEvalAggregatorFactory.validate(): param-types: {}", Arrays.asList(paramType)); + if (!paramType[0].equals(String.class)) { + log.error("CepEvalAggregatorFactory.validate(): Invalid argument #0 type in aggregator '" + aggregatorFunctionName + "'. Expected 'String' but found: " + paramType[0].getName()); + throw new IllegalArgumentException("CepEvalAggregatorFactory.validate(): Invalid argument #0 type in aggregator '"+aggregatorFunctionName+"'. Expected 'String' but found: "+paramType[0].getName()); + } + if (!paramType[1].equals(String.class)) { + log.error("CepEvalAggregatorFactory.validate(): Invalid argument #1 type in aggregator '" + aggregatorFunctionName + "'. Expected 'String' but found: " + paramType[1].getName()); + throw new IllegalArgumentException("CepEvalAggregatorFactory.validate(): Invalid argument #1 type in aggregator '"+aggregatorFunctionName+"'. Expected 'String' but found: "+paramType[1].getName()); + } + for (int i=2; i> ---------------------------------------------------------------------------"); + log.debug(">> eval(map): formula: {}", formula); + log.debug(">> eval(map): streams: {}", streamNames); + log.debug(">> eval(map): entries: {}", maps.length); + log.debug(">> eval(map): maps: {}", java.util.Arrays.asList(maps)); + + String[] names = streamNames.split(","); + if (names.length != maps.length) + throw new IllegalArgumentException("The num. of stream names provided is not equal to the num. of values provided"); + Map args = new HashMap<>(); + for (int i = 0; i < names.length; i++) { + String entryName = names[i].trim(); + Object entryValue = maps[i].get("metricValue"); + log.debug(">> eval(map): maps-entry: {} = {} / {}", entryName, entryValue, entryValue.getClass().getName()); + if (entryValue instanceof String) entryValue = Double.parseDouble((String)entryValue); + args.put(entryName, (Double) entryValue); + } + log.debug(">> eval(map): map-args: {}", args); + + double result = MathUtil.eval(formula, args); + log.debug(">> eval(map): result: {}", result); + + return result; + } + + public static double eval(String formula, String streamNames, Pair pair1) { + return _eval(formula, streamNames, pair1); + } + + public static double eval(String formula, String streamNames, Pair pair1, Pair pair2) { + return _eval(formula, streamNames, pair1, pair2); + } + + public static double eval(String formula, String streamNames, Pair pair1, Pair pair2, Pair pair3) { + return _eval(formula, streamNames, pair1, pair2, pair3); + } + + public static double eval(String formula, String streamNames, Pair pair1, Pair pair2, Pair pair3, Pair pair4) { + return _eval(formula, streamNames, pair1, pair2, pair3, pair4); + } + + public static double _eval(String formula, String streamNames, Pair... pairs) { + log.debug(">> ---------------------------------------------------------------------------"); + log.debug(">> eval(Pair): formula: {}", formula); + log.debug(">> eval(Pair): streams: {}", streamNames); + log.debug(">> eval(Pair): entries: {}", pairs.length); + log.debug(">> eval(Pair): values: {}", Arrays.asList(pairs)); + + String[] names = streamNames.split(","); + if (names.length != pairs.length) + throw new IllegalArgumentException("The num. of stream names provided is not equal to the num. of value pairs provided"); + Map args = new HashMap<>(); + for (int i = 0; i < names.length; i++) { + if (log.isTraceEnabled()) + log.trace(">> eval(Pair): LOOP: i={}, name={}, pair-1={}/{}, pair-2={}/{}", + i, names[i], + pairs[i].getFirst(), pairs[i].getFirst()==null ? null : pairs[i].getFirst().getClass().getName(), + pairs[i].getSecond(), pairs[i].getSecond()==null ? null : pairs[i].getSecond().getClass().getName()); + Object eventObj = pairs[i].getFirst(); + double value; + if (eventObj instanceof EventMap) + value = ((EventMap)eventObj).getMetricValue(); + else if (eventObj instanceof Map) + value = (double) (StrUtil.castToMapStringObject(eventObj)).get("metricValue"); + else if (eventObj instanceof Double) + value = (double) eventObj; + else + throw new IllegalArgumentException("Encountered unsupported Event type in Pair: "+eventObj.getClass().getName()+", event: "+eventObj); + args.put(names[i].trim(), value); + } + log.debug(">> eval(Pair): map-args: {}", args); + + double result = MathUtil.eval(formula, args); + log.debug(">> eval(Pair): result: {}", result); + + return result; + } + + public static double eval(String formula, String streamNames, double... v) { + log.debug(">> ---------------------------------------------------------------------------"); + log.debug(">> eval(double): formula: {}", formula); + log.debug(">> eval(double): streams: {}", streamNames); + log.debug(">> eval(double): entries: {}", v.length); + log.debug(">> eval(double): values: {}", v); + + String[] names = streamNames.split(","); + if (names.length != v.length) + throw new IllegalArgumentException("The num. of stream names provided is not equal to the num. of values provided"); + Map args = new HashMap<>(); + for (int i = 0; i < names.length; i++) args.put(names[i].trim(), v[i]); + log.debug(">> eval(double): map-args: {}", args); + + double result = MathUtil.eval(formula, args); + log.debug(">> eval(double): result: {}", result); + + return result; + } + + public static double evalMath(String formula, double...values) { + log.debug(">> ---------------------------------------------------------------------------"); + log.debug(">> evalMath: formula: {}", formula); + log.debug(">> evalMath: values: {}", values); + + // Get formula arguments + Set argNames = MathUtil.getFormulaArguments(formula); + log.debug(">> evalMath: arg-names: {}", argNames); + + // Check the number of arguments and the number of provided values match + if (argNames.size() != values.length) + throw new IllegalArgumentException(String.format( + "evalMath: The number of provided values do not match the number of formula arguments: #args=%d != #values=%d", + argNames.size(), values.length)); + + // Map values onto arguments, using the order of appearance (i.e. 1st value->1st arg, 2nd value->2nd arg...) + final AtomicInteger i = new AtomicInteger(0); + Map map = argNames.stream().collect(Collectors.toMap( + arg -> arg, arg -> values[ i.getAndIncrement() ] + )); + log.debug(">> evalMath: args-map: {}", map); + + double result = evalMath(formula, map); + log.debug(">> evalMath: result: {}", result); + return result; + } + + public static double evalMath(String formula, Map args) { + log.debug(">> ---------------------------------------------------------------------------"); + log.debug(">> evalMath: formula: {}", formula); + log.debug(">> evalMath: args-map: {}", args); + + double result = MathUtil.eval(formula, args); + log.debug(">> evalMath: result: {}", result); + return result; + } + + public static EventMap newEvent(double metricValue, String... params) { + return newEvent(metricValue, 1, params); + } + + public static EventMap newEvent(double metricValue, int level, String... params) { + log.debug(">> ---------------------------------------------------------------------------"); + log.debug(">> newEvent: metric-value: {}", metricValue); + log.debug(">> newEvent: params-length: {}", params.length); + + // Add metric value + EventMap event = new EventMap(metricValue, level, System.currentTimeMillis()); + + // Add extra parameters + for (int i = 0; i < params.length; i += 2) { + String paramName = params[i]; + String paramValue = params[i + 1]; + event.put(paramName, paramValue); + } + log.debug(">> newEvent: new-event: {}", event); + + return event; + } + +/* public static double eval(String formula, EPLMethodInvocationContext context) { + log.debug(">>>>>>>>>>>>>>>>>> formula: {}", formula); + log.debug(">>>>>>>>>>>>>>>>>> statement-name: {}", context.getStatementName()); + String stmtName = context.getStatementName(); + EPStatement stmt = CepExtensions.cepService.getStatementByName(stmtName); + String stmtText = stmt.getText(); + log.debug(">>>>>>>>>>>>>>>>>> statement-text: {}", stmtText); + + log.debug(">>>>>>>>>>>>>>>>>> statement-streams: {}", CepExtensions.cepService.getStatementStreams(stmtText) ); + + double value = -100*Math.random(); + log.debug(">>>>>>>>>>>>>>>>>> EVAL RESULT: {}", value); + return value; + }*/ + + public static Object prop(Object eventObj, String propertyName) { + return prop(eventObj, propertyName, null); + } + + public static Object prop(Object eventObj, String propertyName, Object defaultValue) { + EventMap event = eventObj instanceof EventMap ? ((EventMap) eventObj) : null; + log.debug(">> ---------------------------------------------------------------------------"); + log.debug(">> prop: event-object: {}", eventObj); + log.debug(">> prop: event-class: {}", eventObj!=null ? eventObj.getClass() : null); + log.debug(">> prop: event-map: {}", event); + log.debug(">> prop: property: {}", propertyName); + + // Retrieve event property + Object ret = null; + if (event!=null) { + Map props = event.getEventProperties(); + if (props != null) { + log.debug(">> prop: properties: {}", props); + ret = props.getOrDefault(propertyName, defaultValue); + defaultValue = null; + } + } + if (ret==null) ret = defaultValue; + log.debug(">> prop: value: {}", ret); + return ret; + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepExtensions.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepExtensions.java new file mode 100644 index 0000000..9344afb --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepExtensions.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.cep; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +/** + * This class registers a few Single-Row Functions as EPL extensions. + * Registered function implementations reside in CepEvalFunction and CepEvalAggregator classes. + * This class is instantiated automatically by Spring-Boot (no need for explicit instantiation) + */ +@Slf4j +@Service +public class CepExtensions { + + // Register Single-Row Functions methods + + @Autowired + public CepExtensions(ApplicationContext appContext) { + CepService cepService = appContext.getBean(CepService.class); + cepService.addSingleRowFunction("EVAL", CepEvalFunction.class.getName(), "eval"); + cepService.addSingleRowFunction("MATH", CepEvalFunction.class.getName(), "evalMath"); + cepService.addSingleRowFunction("NEWEVENT", CepEvalFunction.class.getName(), "newEvent"); + cepService.addSingleRowFunction("PROP", CepEvalFunction.class.getName(), "prop"); + cepService.addAggregatorFunction("EVALAGG", CepEvalAggregatorFactory.class.getName()); + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepService.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepService.java new file mode 100644 index 0000000..0447b29 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/CepService.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.cep; + +import com.espertech.esper.client.*; +import com.google.gson.Gson; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.util.FunctionDefinition; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CepService implements InitializingBean { + + private final Gson gson; + + /** + * Esper service + */ + private EPServiceProvider epService; + + @Override + public void afterPropertiesSet() { + log.debug("CepService: Configuring CEP Service..."); + initService(); + } + + /** + * Start Esper Service + */ + public void initService() { + log.debug("CepService: Initializing CEP Service..."); + Configuration config = new Configuration(); + epService = EPServiceProviderManager.getDefaultProvider(config); + } + + /** + * Dynamic registration of new Event Type using property name and property type arrays + */ + public synchronized void addEventType(String eventTypeName, String[] properties, Class[] propertyTypes) { + log.debug("CepService: Register new Event Type: name={}, properties={}, property-types={}", eventTypeName, properties, propertyTypes); + Map eventTypeDef = new HashMap(); + for (int i = 0; i < properties.length; i++) { + eventTypeDef.put(properties[i], propertyTypes[i]); + } + epService.getEPAdministrator().getConfiguration().addEventType(eventTypeName, eventTypeDef); + } + + /** + * Dynamic registration of new Event Type using event type class + */ + public synchronized void addEventType(String eventTypeName, Class eventType) { + log.debug("CepService: Register new Event Type: name={}, event-type={}", eventTypeName, eventType); + epService.getEPAdministrator().getConfiguration().addEventType(eventTypeName, eventType); + } + + /** + * Clear all registered Event Types + */ + public synchronized void clearEventTypes() { + log.info("CepService: Clear registered Event Types"); + ConfigurationOperations co = epService.getEPAdministrator().getConfiguration(); + EventType[] types = co.getEventTypes(); + for (EventType t : types) { + boolean removed = co.removeEventType(t.getName(), true); + log.info("CepService: Event Type: {} --> removed={}", t.getName(), removed); + } + } + + /** + * Dynamic registration of new EPL statements and corresponding subscribers + */ + public synchronized void addStatementSubscriber(StatementSubscriber subscriber) { + log.debug("CepService: Register EPL statement and subscriber: {}", subscriber.getName()); + String statementStr = subscriber.getStatement(); + log.debug("CepService: EPL statement: {}", statementStr); + EPStatement eventStatement = epService.getEPAdministrator().createEPL(statementStr, subscriber.getName()); + eventStatement.setSubscriber(subscriber); + } + + /** + * Dynamic de-registration of existing EPL statements and corresponding subscribers + */ + public synchronized void removeStatementSubscriber(StatementSubscriber subscriber) { + EPStatement stmt = epService.getEPAdministrator().getStatement(subscriber.getName()); + stmt.stop(); + stmt.destroy(); + } + + /** + * Clear all registered Statements + */ + public synchronized void clearStatements() { + log.info("CepService: Clear registered Statements"); + epService.getEPAdministrator().destroyAllStatements(); + } + + /** + * Get statement by name + */ + public EPStatement getStatementByName(String stmtName) { + log.debug("CepService.getStatementByName(): statement-name={}", stmtName); + return epService.getEPAdministrator().getStatement(stmtName); + } + + /** + * Handle the incoming event as Map + */ + public void handleEvent(Map event, String eventType) { + log.debug("CepService.handleEvent(): type={}, event={}", eventType, event.toString()); + EventMap.checkEvent(event); + epService.getEPRuntime().sendEvent(event, eventType); + } + + /** + * Handle the incoming event as String + */ + public void handleEvent(String event, String eventType) { + log.debug("CepService.handleEvent(): type={}, event={}", eventType, event); + EventMap eventMap = EventMap.parseEventMap(event); + log.trace("CepService.handleEvent(): event-map={}", eventMap); + epService.getEPRuntime().sendEvent(eventMap, eventType); + } + + /** + * Handle the incoming event as Object + */ + public void handleEvent(Object event) { + log.debug("CepService.handleEvent(): event={}", event); + EventMap.checkEvent(StrUtil.castToMapStringObject(event)); + epService.getEPRuntime().sendEvent(event); + } + + /** + * Add a user-defined aggregator function in Esper + */ + public void addAggregatorFunction(String functionName, String aggregationFactoryClassName) { + log.debug("CepService.addAggregatorFunction(): function={}, aggregator-factory-class={}", functionName, aggregationFactoryClassName); + epService.getEPAdministrator().getConfiguration().addPlugInAggregationFunctionFactory(functionName, aggregationFactoryClassName); + } + + /** + * Add a user-defined single-row function in Esper + */ + public void addSingleRowFunction(String functionName, String className, String methodName) { + log.debug("CepService.addSingleRowFunction(): function={}, class={}, method={}", functionName, className, methodName); + /*epService.getEPAdministrator().getConfiguration().addPlugInSingleRowFunction(functionName, className, methodName, + com.espertech.esper.client.ConfigurationPlugInSingleRowFunction.ValueCache.CONFIGURED, //enum: ENABLED, DISABLED, CONFIGURED + com.espertech.esper.client.ConfigurationPlugInSingleRowFunction.FilterOptimizable.ENABLED, //enum: ENABLED, DISABLED + true // re-throw exceptions + );*/ + com.espertech.esper.client.ConfigurationPlugInSingleRowFunction entry = new com.espertech.esper.client.ConfigurationPlugInSingleRowFunction(); + entry.setName(functionName); + entry.setFunctionClassName(className); + entry.setFunctionMethodName(methodName); + entry.setRethrowExceptions(true); + epService.getEPAdministrator().getConfiguration().addPlugInSingleRowFunction(entry); + } + + /** + * Get statement streams (i.e. FROM clause stream names. Non-named streams (those without AS) return 'null') + */ + public List getStatementStreams(String statementText) { + log.debug("CepService.getStatementStreams(): statement={}", statementText); + return epService.getEPAdministrator().compileEPL(statementText).getFromClause().getStreams().stream().map(stream -> stream.getStreamName()).collect(Collectors.toList()); + } + + /** + * Add a function definition in MathParser + */ + public void addFunctionDefinition(FunctionDefinition functionDef) { + log.debug("CepService.addFunctionDefinition(): Add new function definition: {}", functionDef); + MathUtil.addFunctionDefinition(functionDef); + } + + /** + * Clear function definitions in MathParser + */ + public void clearFunctionDefinitions() { + log.debug("CepService.clearFunctionDefinitions(): Clear function definitions"); + MathUtil.clearFunctionDefinitions(); + } + + /** + * Add/Set a constant in MathParser + */ + public void setConstant(String constName, double constValue) { + log.debug("CepService.setConstant(): Add/Set constant: name={}, value={}", constName, constValue); + MathUtil.setConstant(constName, constValue); + } + + /** + * Add/Set constants in a map, in MathParser + */ + public void setConstants(Map constants) { + log.debug("CepService.setConstants(): Add/Set constants in map: {}", constants); + MathUtil.setConstants(constants); + } + + /** + * Clear constants in MathParser + */ + public void clearConstants() { + log.debug("CepService.clearConstants(): Clear constants"); + MathUtil.clearConstants(); + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/MathUtil.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/MathUtil.java new file mode 100644 index 0000000..9992666 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/MathUtil.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.cep; + +import gr.iccs.imu.ems.util.FunctionDefinition; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.mariuszgromada.math.mxparser.Constant; +import org.mariuszgromada.math.mxparser.Expression; +import org.mariuszgromada.math.mxparser.Function; +import org.mariuszgromada.math.mxparser.mXparser; +import org.mariuszgromada.math.mxparser.parsertokens.FunctionVariadic; +import org.mariuszgromada.math.mxparser.parsertokens.Token; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Slf4j +public class MathUtil { + private static Map functions = new HashMap<>(); + private static Map constants = new HashMap<>(); + + // ------------------------------------------------------------------------ + + public static void addFunctionDefinition(FunctionDefinition functionDef) { + log.debug("MathUtil: Add new function definition: {}", functionDef); + String argsStr = String.join(", ", functionDef.getArguments()); + //String defStr = String.format("%(%s) = %s", functionDef.getName(), argsStr, functionDef.getExpression()); + String defStr = functionDef.getName() + "(" + argsStr + ") = " + functionDef.getExpression(); + log.debug("MathUtil: definition-string: {}", defStr); + Function func = new Function(defStr); + functions.put(functionDef.getName(), func); + } + + public static void clearFunctionDefinitions() { + log.debug("MathUtil: Clear function definitions"); + functions.clear(); + } + + // ------------------------------------------------------------------------ + + public static void setConstant(String constantName, double constantValue) { + log.debug("MathUtil: Set constant: name={}, value={}", constantName, constantValue); + Constant con = new Constant(constantName, constantValue); + constants.put(constantName, con); + } + + public static void setConstants(Map constantsMap) { + log.debug("MathUtil: Add constants using map: {}", constantsMap); + //constantsMap.entrySet().stream().forEach(c -> setConstant(c.getKey(), c.getValue())); + constantsMap.forEach(MathUtil::setConstant); + } + + public static void clearConstants() { + log.debug("MathUtil: Clear constants"); + constants.clear(); + } + + // ------------------------------------------------------------------------ + + public static @NonNull Set getFormulaArguments(String formula) { + log.debug("MathUtil: getFormulaArguments: formula={}", formula); + if (StringUtils.isBlank(formula)) { + log.debug("MathUtil: getFormulaArguments: Formula is null or empty"); + return Collections.emptySet(); + } + + // Create MathParser expression + Expression e = new Expression(formula); + //e.setVerboseMode(); + log.trace("MathUtil: getFormulaArguments: expression={}", e.getExpressionString()); + + Set argNames = extractArgNames(e); + log.debug("MathUtil: getFormulaArguments: arguments={}", argNames); + + return argNames; + } + + private static @NonNull Set extractArgNames(Expression e) { + + List initTokens = extractFormulaTokens(e); + + Set argNames = initTokens.stream() + .filter(t -> t.tokenTypeId == Token.NOT_MATCHED) + .filter(t -> "argument".equals(t.looksLike)) + .map(t -> t.tokenStr) + .collect(Collectors + .toCollection(LinkedHashSet::new)); + log.debug("MathUtil: initial-token-names: {}", argNames); + + return argNames; + } + + private static @NonNull List extractFormulaTokens(Expression e) { + // Add constants + e.addConstants(new ArrayList<>(constants.values())); + + // Add functions + for (Function f : functions.values()) e.addFunctions(f); + + // Check syntax + boolean lexSyntax = e.checkLexSyntax(); + boolean genSyntax = e.checkSyntax(); + if (log.isTraceEnabled()) { + log.trace("MathUtil: lexSyntax={}, genSyntax: {}", lexSyntax, genSyntax); + log.trace("MathUtil: syntax-status={}, error={}", e.getSyntaxStatus(), e.getErrorMessage()); + } + + // Get token names + List initTokens = e.getCopyOfInitialTokens(); + log.debug("MathUtil: initial-tokens={}", initTokens); + if (log.isTraceEnabled()) { + mXparser.consolePrintTokens(initTokens); + } + + return initTokens; + } + + // ------------------------------------------------------------------------ + + public static boolean containsAggregator(String formula) { + log.debug("MathUtil: containsAggregator: formula={}", formula); + if (StringUtils.isBlank(formula)) { + log.debug("MathUtil: containsAggregator: Formula is null or empty"); + return false; + } + + // Create MathParser expression + Expression e = new Expression(formula); + //e.setVerboseMode(); + log.trace("MathUtil: containsAggregator: expression={}", e.getExpressionString()); + + // Get formula tokens + List initTokens = extractFormulaTokens(e); + + // Select 'function' names from tokens + List names = initTokens.stream() + .filter(t -> t.tokenTypeId == FunctionVariadic.TYPE_ID) + .map(t -> t.tokenStr) + .collect(Collectors.toList()); + log.trace("MathUtil: containsAggregator: formula-aggregator-functions: {}", names); + + // Check if aggregators exist + boolean containsAgg = names.size() > 0; + if (containsAgg) + log.debug("MathUtil: containsAggregator: Formula contains aggregators: aggregators={}, formula={}", names, formula); + else + log.debug("MathUtil: containsAggregator: Formula does not contain aggregators: {}", formula); + return containsAgg; + } + + // ------------------------------------------------------------------------ + + protected final static String[] aggregatorNames = {"iff", "min", "max", "ConFrac", "ConPol", "gcd", "lcm", "add", "multi", "mean", "var", "std", "rList"}; + + public static boolean containsAggregatorRegexp(String formula) { + log.debug("MathUtil: containsAggregatorRegexp: formula={}", formula); + if (StringUtils.isBlank(formula)) { + log.debug("MathUtil: containsAggregatorRegexp: Formula is null or empty"); + return false; + } + formula = " " + formula; + for (int i = 0; i < aggregatorNames.length; i++) { + log.trace("MathUtil: containsAggregatorRegexp: checking aggregator: aggregator={}, formula={}", aggregatorNames[i], formula); + if (checkPattern(formula, aggregatorNames[i])) { + log.debug("MathUtil: containsAggregatorRegexp: Formula contains aggregators: aggregator={}, formula={}", aggregatorNames[i], formula); + return true; + } + } + log.debug("MathUtil: containsAggregatorRegexp: Formula does not contain aggregators: formula={}", formula); + return false; + } + + protected static boolean checkPattern(String formula, String aggregatorName) { + int flags = Pattern.CASE_INSENSITIVE; + Pattern pat = Pattern.compile(String.format("[^a-zA-Z]%s[^a-zA-Z]", aggregatorName), flags); + return pat.matcher(formula).find(); + } + + // ------------------------------------------------------------------------ + + public static double evalAgg(String formula, Map> argsMap) { + log.debug("MathUtil: evalAgg: input: formula={}, arg-map={}", formula, argsMap); + int iter = 0; + for (Map.Entry> arg : argsMap.entrySet()) { + log.debug("MathUtil: evalAgg: iteration #{}: arg={}", iter, arg); + String argName = arg.getKey(); + List argValue = arg.getValue(); + log.debug("MathUtil: evalAgg: iteration #{}: arg-name={}, arg-value={}", iter, argName, argValue); + String valStr = argValue.stream().map(value -> value.toString()).collect(Collectors.joining(", ")); + log.debug("MathUtil: evalAgg: iteration #{}: arg-name={}, arg-value-str={}", iter, argName, valStr); + + formula = formula.replaceAll(argName, valStr); + iter++; + } + log.debug("MathUtil: evalAgg: formula-to-evaluate: {}", formula); + + return eval(formula, new java.util.HashMap<>()); + } + + public static double eval(String formula, Map argsMap) { + // Create MathParser expression + Expression e = new Expression(formula); + //e.setVerboseMode(); + log.debug("MathUtil: formula={}", e.getExpressionString()); + + // Get argument names + Set argNames = extractArgNames(e); + + // Define expression arguments with user provided values + //e.removeAllArguments(); + for (String argName : argNames) { + try { + log.debug("MathUtil: Defining Arg: {}", argName); + double argValue = argsMap.get(argName); + e.defineArgument(argName, argValue); + log.debug("MathUtil: Arg: {} = {}", argName, argValue); + } catch (Exception ex) { + log.error("MathUtil: Defining Arg: EXCEPTION: arg-name={}, args-map={}", argName, argsMap); + throw ex; + } + } + boolean genSyntax = e.checkSyntax(); + if (!genSyntax) + throw new IllegalArgumentException("Syntax error in expression: " + e.getErrorMessage()); + + // Calculate result + double result = e.calculate(); + log.debug("MathUtil: Result={}, computing-time={}, error={}", result, e.getComputingTime(), e.getErrorMessage()); + + if (Double.isInfinite(result) || Double.isNaN(result)) { + log.warn("MathUtil: ------------------------------------------------------------------------"); + log.warn("MathUtil: Result is NaN or Infinite: result={}", result); + log.warn("MathUtil: Context: formula: {}", formula); + log.warn("MathUtil: Context: args-map: {}", argsMap); + log.warn("MathUtil: Context: constants: {}", constants.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, x->x.getValue().getConstantValue() + ))); + log.warn("MathUtil: Context: functions: {}", functions.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, x->x.getValue().getFunctionExpressionString() + ))); + log.warn("MathUtil: ------------------------------------------------------------------------"); + throw new IllegalStateException("MathUtil.eval result is NaN or Infinite: "+result); + } + + return result; + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/StatementSubscriber.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/StatementSubscriber.java new file mode 100644 index 0000000..f5e2eec --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/cep/StatementSubscriber.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.cep; + +/** + * A convenience interface to let us easily contain the Esper statements with the Subscribers - + * just for clarity so it's easy to see the statements the subscribers are registered against. + */ +public interface StatementSubscriber { + + /** + * Get the Subscriber name. + * + * @return Subscriber name + */ + String getName(); + + /** + * Get the EPL Statement the Subscriber will listen to. + * + * @return EPL Statement + */ + String getStatement(); +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/event/EventMap.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/event/EventMap.java new file mode 100644 index 0000000..c38b5ff --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/event/EventMap.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.event; + +import com.google.gson.Gson; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Data +@Slf4j +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class EventMap extends LinkedHashMap implements Serializable { + + private static Gson gson; + private static AtomicLong eventIdSequence = new AtomicLong(0); + + // Standard/Known Event fields configuration + @Data + public static class EventField { + private final String name; + private final Class type; + private final boolean nullable; + private final boolean skipIfNull; + private final Function parser; + private final Function defaultValue; + } + + public final static String METRIC_VALUE_NAME = "metricValue"; + public final static String LEVEL_NAME = "level"; + public final static String TIMESTAMP_NAME = "timestamp"; + + public final static List STANDARD_EVENT_FIELDS = Collections.unmodifiableList(Arrays.asList( + new EventField(METRIC_VALUE_NAME, Double.class, false, false, Double::parseDouble, null), + new EventField(LEVEL_NAME, Integer.class, true, true, (v)->(int)Double.parseDouble(v), null), + new EventField(TIMESTAMP_NAME, Long.class, true, true, v->(long)Double.parseDouble(v), (f)->System.currentTimeMillis()) + )); + + public final static Map STANDARD_EVENT_FIELDS_MAP = Collections.unmodifiableMap( + STANDARD_EVENT_FIELDS.stream().collect(Collectors.toMap(EventField::getName, x->x))); + + public final static String[] PROPERTY_NAMES_ARRAY = STANDARD_EVENT_FIELDS.stream() + .map(EventField::getName).collect(Collectors.toList()).toArray(new String[0]); + public final static Class[] PROPERTY_CLASSES_ARRAY = STANDARD_EVENT_FIELDS.stream() + .map(EventField::getType).collect(Collectors.toList()).toArray(new Class[0]); + + public static String[] getPropertyNames() { + return PROPERTY_NAMES_ARRAY; + } + + public static Class[] getPropertyClasses() { + return PROPERTY_CLASSES_ARRAY; + } + + + // Event Id + private final long eventId = eventIdSequence.getAndIncrement(); + + // Event properties + private Map eventProperties; + + public Object getEventProperty(@NonNull String name) { + return eventProperties.get(name); + } + + public synchronized Object setEventProperty(@NonNull String name, Object value) { + if (eventProperties == null) eventProperties = new LinkedHashMap<>(); + return eventProperties.put(name, value); + } + + // Constructors + /*public EventMap() { + super(); + put(TIMESTAMP_NAME, System.currentTimeMillis()); + }*/ + + public EventMap(Map map) { + checkEvent(map); + map.forEach((k, v) -> { + log.trace("EventMap.: key={}, value={}", k, v); + this.put(k, v); + }); + if (map instanceof EventMap) { + Map properties = ((EventMap) map).getEventProperties(); + if (properties!=null && properties.size()>0) + setEventProperties(new LinkedHashMap<>(properties)); + } + checkEvent(); + } + + public EventMap(double metricValue) { + put(METRIC_VALUE_NAME, metricValue); + put(TIMESTAMP_NAME, System.currentTimeMillis()); + checkEvent(); + } + + public EventMap(double metricValue, long timestamp) { + put(METRIC_VALUE_NAME, metricValue); + put(TIMESTAMP_NAME, timestamp); + checkEvent(); + } + + public EventMap(double metricValue, int level, long timestamp) { + put(METRIC_VALUE_NAME, metricValue); + put(LEVEL_NAME, level); + put(TIMESTAMP_NAME, timestamp); + checkEvent(); + } + + + // Convert Object to EventMap + public static EventMap toEventMap(@NonNull Object o) { + if (o instanceof EventMap) return (EventMap) o; + if (o instanceof Map) return new EventMap(StrUtil.castToMapStringObject(o) ); + return parseEventMap(o.toString()); + } + + // Parse from string + public static EventMap parseEventMap(@NonNull String s) { + /*if (s==null) return null; + s = s.trim(); + if (s.isEmpty()) return null; + if (s.startsWith("{") && s.endsWith("}")) s = s.substring(1, s.length()-1).trim(); + String[] pairs = s.split(","); + EventMap eventMap = new EventMap(); + for (String pair : pairs) { + if (StringUtils.isBlank(pair)) + continue; + String[] kv = pair.split("[:=]", 2); + if (kv.length==2) + eventMap.put(kv[0], kv[1]); + else + eventMap.put(kv[0], null); + } + return eventMap; + */ + EventMap eventMap = gson.fromJson(s, EventMap.class); + eventMap.checkEvent(); + return eventMap; + } + + public void checkEvent() { + checkEvent(this); + } + + public static void checkEvent(Map map) { + // Check metric value + Object m = map.get(METRIC_VALUE_NAME); + if (m==null) throw new IllegalArgumentException("Argument does not contain a 'metricValue'"); + if (!(m instanceof Number n)) throw new IllegalArgumentException("Argument contains a non-numeric 'metricValue' : "+m); + else { + double d = n.doubleValue(); + if (Double.isInfinite(d) || Double.isNaN(d)) throw new IllegalArgumentException("Argument contains NaN or Infinite 'metricValue' : "+m); + } + // Check level value + // Check timestamp value + } + + + // Methods overridden + @Override + public Object put(String key, Object value) { + log.trace("EventMap.put(): BEGIN: key={}, value={}", key, value); + key = removeQuotes(key); + log.trace("EventMap.put(): KEY with Quotes Stripped: key={}", key); + + // Process known (standard) event fields + EventField field = STANDARD_EVENT_FIELDS_MAP.get(key); + if (field!=null) { + log.trace("EventMap.put(): STANDARD_EVENT_FIELD: key={}, value={}", key, value); + if (value==null) { + if (!field.isNullable()) + throw new NullPointerException("Event field cannot be null: " + key); + if (field.isSkipIfNull()) return null; + value = field.getDefaultValue().apply(this); + log.debug("EventMap.put(): Assigned default value to: key={}, value={}", key, value); + } + if (!field.getType().isInstance(value)) { + log.trace("EventMap.put(): Value type is different than Event field type: key={}, value={}, value-type={}, field-type={}", + key, value, value.getClass().getName(), field.getType().getName()); + value = field.getParser().apply(removeQuotes(value)); + log.debug("EventMap.put(): Value after parsing: key={}, value={}, value-type={}, field-type={}", + key, value, value.getClass().getName(), field.getType().getName()); + } + } + + log.trace("EventMap.put(): Putting in EventMap: key={}, value={}", key, value); + return super.put(key, value); + } + + protected static String removeQuotes(@NonNull Object o) { + String s = o.toString(); + int l = s.length()-1; + s = (s.charAt(0)=='"' && s.charAt(l)=='"' || s.charAt(0)=='\'' && s.charAt(l)=='\'') + ? s.substring(1, l) : s; + log.trace("EventMap.removeQuotes(): INPUT={}, RESULT={}", o, s); + return s; + } + + // Getters for standard event fields + public double getMetricValue() { + Object v = get(METRIC_VALUE_NAME); + if (v==null) + throw new NullPointerException("No '"+METRIC_VALUE_NAME+"' found in EventMap: "+this); + if (v instanceof Double) return (Double) v; + if (v instanceof Number) return ((Number)v).doubleValue(); + return Double.parseDouble(removeQuotes(v)); + } + + public long getTimestamp() { + Object v = get(TIMESTAMP_NAME); + if (v==null) + throw new NullPointerException("No '"+TIMESTAMP_NAME+"' found in EventMap: "+this); + if (v instanceof Long) return (Long) v; + if (v instanceof Number) return ((Number)v).longValue(); + return Long.parseLong(removeQuotes(v)); + } + + public Map getPayload() { + return new LinkedHashMap<>(this); + } + + public String toString() { + return getEventProperties()!=null + ? "{ payload: "+super.toString() + ", properties: " + getEventProperties().toString() + " }" + : super.toString(); + } + + public String toJsonString() { + return gson.toJson(this); + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/event/EventRecorder.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/event/EventRecorder.java new file mode 100644 index 0000000..c9c7a64 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/event/EventRecorder.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.event; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import gr.iccs.imu.ems.brokercep.properties.BrokerCepProperties; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.command.ActiveMQMessage; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.springframework.scheduling.TaskScheduler; + +import javax.jms.*; +import javax.jms.Queue; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Serializable; +import java.lang.IllegalStateException; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ScheduledFuture; + +@Slf4j +public class EventRecorder extends LinkedHashMap implements Runnable { + public enum FORMAT { JSON, CSV } + + private final static Object staticLock = new Object(); + public static Set activeEventRecorders; + + @Getter + private final FORMAT recordFormat; + @Getter + private final String recordFilePattern; + @Getter + private final BrokerCepProperties.EVENT_RECORDER_FILTER_MODE filterMode; + @Getter + private final List allowedDestinations; + + @Getter + private String recordFile; + @Getter + private boolean closed; + @Getter + private boolean recording; + + private BufferedWriter recordWriter; + private CSVPrinter csvPrinter; + private JsonGenerator jsonGenerator; + + private final Deque eventQueue; + private final TaskScheduler scheduler; + private ScheduledFuture runnerFuture; + + public EventRecorder(@NonNull BrokerCepProperties.EventRecorderProperties properties, @NonNull TaskScheduler scheduler) throws IOException { + this(properties.getFormat(), properties.getFile(), properties.getFilterMode(), properties.getAllowedDestinations(), scheduler); + } + + public EventRecorder(@NonNull FORMAT recordFormat, @NonNull String recordFilePattern, BrokerCepProperties.EVENT_RECORDER_FILTER_MODE filterMode, List allowedDestinations, @NonNull TaskScheduler scheduler) throws IOException { + this.recordFormat = recordFormat; + this.recordFilePattern = recordFilePattern; + this.filterMode = filterMode; + this.allowedDestinations = allowedDestinations==null ? Collections.emptyList() : Collections.unmodifiableList(allowedDestinations); + this.scheduler = scheduler; + this.eventQueue = new ConcurrentLinkedDeque<>(); + + registerShutdownHook(); + rotate(); + } + + public static void registerShutdownHook() { + if (activeEventRecorders==null) { + synchronized (staticLock) { + if (activeEventRecorders==null) { + activeEventRecorders = new HashSet<>(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + log.info("EventRecorder: closing active recorders: {}", activeEventRecorders.size()); + for (EventRecorder eventRecorder : activeEventRecorders) { + if (!eventRecorder.isClosed()) + eventRecorder.close(); + } + log.info("EventRecorder: Closed active recorders"); + })); + } + } + } + } + + public synchronized void rotate() throws IOException { + // Close current recording file + if (recordFile!=null && !isClosed()) { + close(); + } + closed = false; + + // Create new recording file + this.recordFile = recordFilePattern + .replace("%T", "" + System.currentTimeMillis()) + .replace("%S", getSuffix()); + this.recordWriter = new BufferedWriter(new FileWriter(recordFile)); + + //log.info("EventRecorder: Record format: {}", recordFormat); + //log.info("EventRecorder: Record file: {}", recordFile); + log.info("EventRecorder: Record format: {}, Record file: {}", recordFormat, recordFile); + + if (recordFormat==FORMAT.CSV) { + csvPrinter = new CSVPrinter(recordWriter, CSVFormat.DEFAULT + .withHeader("Timestamp", "Destination", "Mime", "Type", "Contents", "Properties")); + csvPrinter.flush(); + } + if (recordFormat==FORMAT.JSON) { + jsonGenerator = new JsonFactory() + .createGenerator(recordWriter) + .setPrettyPrinter(new DefaultPrettyPrinter()); + jsonGenerator.writeStartArray(); + jsonGenerator.flush(); + } + + // Start processing loop + runnerFuture = scheduler.scheduleAtFixedRate(this, Duration.ofMillis(1000)); + activeEventRecorders.add(this); + + startRecording(); + } + + private String getSuffix() { + if (recordFormat==FORMAT.JSON) return "json"; + if (recordFormat==FORMAT.CSV) return "csv"; + throw new IllegalStateException("No suffix for FORMAT: "+recordFormat); + } + + public synchronized void close() { + if (closed) throw new IllegalStateException("EventRecorder has already been closed"); + if (recording) stopRecording(); + this.closed = true; + runnerFuture.cancel(false); + activeEventRecorders.remove(this); + + // wait until all records are written in the file + while (!eventQueue.isEmpty()) { + run(); + } + + // close record file + try { + if (recordFormat == FORMAT.CSV) { + csvPrinter.close(true); + } + if (recordFormat == FORMAT.JSON) { + jsonGenerator.writeEndArray(); + jsonGenerator.close(); + } + recordWriter.close(); + } catch (Exception ex) { + log.warn("EventRecorder: Exception while closing: ", ex); + } + } + + public void startRecording() { + if (closed) throw new IllegalStateException("EventRecorder has been closed"); + if (!recording) { + log.info("EventRecorder: Start recording..."); + recording = true; + } + } + + public void stopRecording() { + if (closed) throw new IllegalStateException("EventRecorder has been closed"); + if (recording) { + log.info("EventRecorder: Stop recording..."); + recording = false; + } + } + + public void recordEvent(@NonNull ActiveMQMessage message) throws JMSException { + recordAllowedEvent(message); + } + + public void recordAllowedEvent(@NonNull Message message) throws JMSException { + if (filterMode == BrokerCepProperties.EVENT_RECORDER_FILTER_MODE.ALL + || filterMode == BrokerCepProperties.EVENT_RECORDER_FILTER_MODE.ALLOWED + && allowedDestinations.stream().anyMatch(getDestinationName(message)::equalsIgnoreCase)) + { + eventQueue.addLast(message); + } + } + + public void recordRegisteredEvent(@NonNull Message message) { + if (filterMode==BrokerCepProperties.EVENT_RECORDER_FILTER_MODE.REGISTERED) { + eventQueue.addLast(message); + } + } + + public void run() { + if (!closed) { + while (!eventQueue.isEmpty()) { + try { + processEvent(eventQueue.removeLast()); + } catch (Exception ex) { + log.warn("EventRecorder: Exception while processing event queue: ", ex); + } + } + } + } + + protected void processEvent(Message message) throws IOException, JMSException { + String messageId = message.getJMSMessageID(); + long timestamp = message.getJMSTimestamp(); + String destinationName = getDestinationName(message); + String mime = message.getJMSType(); + + // Extract event payload and type + PayloadAndType payloadAndType = extractPayloadAndType(message); + String content = payloadAndType.payload; + String type = payloadAndType.type; + + // Extract event properties + String properties = extractProperties(message); + + if (recordFormat==FORMAT.CSV) { + csvPrinter.printRecord(timestamp, destinationName, mime, type, content, properties); + csvPrinter.flush(); + } + if (recordFormat==FORMAT.JSON) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("id", messageId); + jsonGenerator.writeNumberField("timestamp", timestamp); + jsonGenerator.writeStringField("destination", destinationName); + jsonGenerator.writeStringField("mime", mime); + jsonGenerator.writeStringField("type", type); + jsonGenerator.writeStringField("content", content); + jsonGenerator.writeStringField("properties", properties); + jsonGenerator.writeEndObject(); + jsonGenerator.flush(); + } + } + + protected String getDestinationName(Message message) throws JMSException { + Destination d = message.getJMSDestination(); + if (d instanceof Topic) { + return ((Topic)d).getTopicName(); + } else + if (d instanceof Queue) { + return ((Queue)d).getQueueName(); + } else + throw new IllegalArgumentException("Argument is not a JMS destination: "+d); + } + + protected PayloadAndType extractPayloadAndType(Message message) throws JMSException { + if (message instanceof TextMessage) { + return new PayloadAndType("TEXT", ((TextMessage)message).getText()); + } else + if (message instanceof ObjectMessage) { + Serializable o = ((ObjectMessage) message).getObject(); + return new PayloadAndType("OBJECT", o==null ? null : o.toString()); + } else + throw new IllegalArgumentException("Unsupported message type: "+message.getClass().getName()); + } + + protected String extractProperties(Message message) throws JMSException { + Enumeration en = message.getPropertyNames(); + StringBuilder properties = new StringBuilder("{"); + boolean first = true; + while (en.hasMoreElements()) { + Object k = en.nextElement(); + if (k!=null) { + String v = message.getStringProperty(k.toString()); + if (first) first = false; else properties.append(", "); + properties.append(k).append("=").append(v); + } + } + properties.append(" }"); + return properties.toString(); + } + + @AllArgsConstructor + class PayloadAndType { + public String type; + public String payload; + } +} \ No newline at end of file diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/properties/BrokerCepProperties.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/properties/BrokerCepProperties.java new file mode 100644 index 0000000..72263be --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/properties/BrokerCepProperties.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.properties; + +import gr.iccs.imu.ems.brokercep.EventCache; +import gr.iccs.imu.ems.brokercep.event.EventRecorder; +import gr.iccs.imu.ems.util.EmsConstant; +import gr.iccs.imu.ems.util.KeystoreAndCertificateProperties; +import gr.iccs.imu.ems.util.NetUtil; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; +//import org.springframework.context.annotation.PropertySource; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Data +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "brokercep") +//@PropertySource("file:${EMS_CONFIG_DIR}/gr.iccs.imu.ems.brokercep.properties") +public class BrokerCepProperties implements InitializingBean { + public void afterPropertiesSet() { + log.debug("BrokerCepProperties: {}", this); + } + + private String brokerName = "broker"; + + // Broker connector URLs + private List brokerUrl = Collections.singletonList("ssl://0.0.0.0:61616"); + public String getBrokerUrl() { return brokerUrl.get(0); } + public List getBrokerUrlList() { return brokerUrl; } + + private String brokerUrlForConsumer = "ssl://127.0.0.1:61616"; + private String brokerUrlForClients = "ssl://"+ NetUtil.getPublicIpAddress()+":61616"; + + private int managementConnectorPort = -1; + private boolean bypassLocalBroker; + private long eventForwarderLoopDelay = 100L; + + // brokercep.ssl.** settings + @NestedConfigurationProperty + private KeystoreAndCertificateProperties ssl; + + private boolean authenticationEnabled; + @ToString.Exclude + private String additionalBrokerCredentials; + private boolean authorizationEnabled; + + private boolean brokerPersistenceEnabled; + private boolean brokerUsingJmx; + private boolean brokerAdvisorySupportEnabled; + private boolean brokerUsingShutdownHook; + + private boolean brokerEnableStatistics; + private boolean brokerPopulateJmsxUserId; + + private boolean enableAdvisoryWatcher = true; + private int advisoryWatcherInitRetryDelay = 5; // in seconds + + private List messageInterceptors; + private Map messageInterceptorsSpecs = new HashMap<>(); + + private List messageForwardDestinations = Collections.emptyList(); + + private int maxEventForwardRetries = -1; + private long maxEventForwardDuration = -1; + + private Usage usage = new Usage(); + + private boolean logBrokerMessages = true; + private boolean logBrokerMessagesFull = false; + + private EventRecorderProperties eventRecorder = new EventRecorderProperties(); + + private boolean eventCacheEnabled = true; + private int eventCacheSize = EventCache.DEFAULT_EVENT_CACHE_SIZE; + + @Data + public static class Usage { + private Memory memory = new Memory(); + } + @Data + public static class Memory { + private int jvmHeapPercentage = -1; + private long size = -1; + } + @Data + public static class MessageInterceptorSpec { + private String className; + private List params; + } + @Data + @ToString(callSuper = true) + @EqualsAndHashCode(callSuper = true) + public static class MessageInterceptorConfig extends MessageInterceptorSpec { + private String destination; + } + @Data + public static class ForwardDestinationConfig { + private String connectionString; + private String username; + @ToString.Exclude + private String password; + } + + public enum EVENT_RECORDER_FILTER_MODE { ALL, REGISTERED, ALLOWED } + + @Data + public static class EventRecorderProperties { + private boolean enabled; + private EventRecorder.FORMAT format = EventRecorder.FORMAT.CSV; + private String file; + private EVENT_RECORDER_FILTER_MODE filterMode = EVENT_RECORDER_FILTER_MODE.REGISTERED; + private List allowedDestinations; + } +} diff --git a/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/properties/NodeProperties.java b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/properties/NodeProperties.java new file mode 100644 index 0000000..ed6d412 --- /dev/null +++ b/ems-core/broker-cep/src/main/java/gr/iccs/imu/ems/brokercep/properties/NodeProperties.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokercep.properties; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Data +@Component +@ConfigurationProperties +public class NodeProperties implements InitializingBean { + public void afterPropertiesSet() { + log.debug("NodeProperties: {}", this); + } + + private boolean addNodePropertiesToEventsEnabled = true; + private boolean skipNullValues; + private boolean skipBlankValues; + + private Map nodeProperties = new HashMap<>(); +} diff --git a/ems-core/broker-client/client.bat b/ems-core/broker-client/client.bat new file mode 100644 index 0000000..56edaba --- /dev/null +++ b/ems-core/broker-client/client.bat @@ -0,0 +1,22 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +set EMS_CONFIG_DIR=. + +setlocal +rem set JAVA_OPTS= -Djavax.net.ssl.trustStore=..\config-files\broker-truststore.p12 ^ +rem -Djavax.net.ssl.trustStorePassword=melodic ^ +rem -Djavax.net.ssl.trustStoreType=pkcs12 +rem -Djavax.net.debug=all +rem -Djavax.net.debug=ssl,handshake,record + +java %JAVA_OPTS% -jar target\broker-client-jar-with-dependencies.jar %* + +endlocal diff --git a/ems-core/broker-client/client.sh b/ems-core/broker-client/client.sh new file mode 100644 index 0000000..9ba5d10 --- /dev/null +++ b/ems-core/broker-client/client.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +EMS_CONFIG_DIR=. + +#JAVA_OPTS=-Djavax.net.ssl.trustStore=./broker-truststore.p12\ -Djavax.net.ssl.trustStorePassword=melodic\ -Djavax.net.ssl.trustStoreType=pkcs12 +# -Djavax.net.debug=all +# -Djavax.net.debug=ssl,handshake,record + +java $JAVA_OPTS -jar target/broker-client-jar-with-dependencies.jar $* diff --git a/ems-core/broker-client/pom.xml b/ems-core/broker-client/pom.xml new file mode 100644 index 0000000..2f67b36 --- /dev/null +++ b/ems-core/broker-client/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + broker-client + EMS - Broker Client library + + + + + gr.iccs.imu.ems + util + ${project.version} + + + + + + org.springframework + spring-jms + + + org.apache.activemq + activemq-client + ${activemq.version} + + + + org.apache.activemq + activemq-broker + ${activemq.version} + + + + + org.projectlombok + lombok + provided + + + + + org.apache.commons + commons-lang3 + + + + + org.apache.commons + commons-csv + 1.7 + + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.1 + + + + jar-with-dependencies + + + + gr.iccs.imu.ems.brokerclient.BrokerClientApp + true + true + + + + + + + make-assembly + package + + single + + + + + + + + + diff --git a/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/BrokerClient.java b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/BrokerClient.java new file mode 100644 index 0000000..b199075 --- /dev/null +++ b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/BrokerClient.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokerclient; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import gr.iccs.imu.ems.brokerclient.event.EventMap; +import gr.iccs.imu.ems.brokerclient.properties.BrokerClientProperties; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.ActiveMQConnection; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.ActiveMQSslConnectionFactory; +import org.apache.activemq.advisory.DestinationSource; +import org.apache.activemq.command.ActiveMQQueue; +import org.apache.activemq.command.ActiveMQTempQueue; +import org.apache.activemq.command.ActiveMQTempTopic; +import org.apache.activemq.command.ActiveMQTopic; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.jms.*; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.nio.file.Paths; +import java.util.*; + +@Slf4j +@Component +public class BrokerClient { + + @Autowired + private BrokerClientProperties properties; + @Autowired + private PasswordUtil passwordUtil; + private Connection connection; + private Session session; + private HashMap listeners = new HashMap<>(); + private Gson gson = new GsonBuilder().create(); + + public BrokerClient() { + } + + public BrokerClient(BrokerClientProperties bcp) { + properties = bcp; + } + + public BrokerClient(Properties p) { + properties = new BrokerClientProperties(p); + } + + public BrokerClient(PasswordUtil pu) { + passwordUtil = pu; + } + + public BrokerClient(BrokerClientProperties bcp, PasswordUtil pu) { + properties = bcp; + passwordUtil = pu; + } + + public BrokerClient(Properties p, PasswordUtil pu) { + properties = new BrokerClientProperties(p); + passwordUtil = pu; + } + + // ------------------------------------------------------------------------ + + public static BrokerClient newClient() throws java.io.IOException, JMSException { + log.info("BrokerClient: Initializing..."); + + // get properties file + String configDir = System.getenv("EMS_CONFIG_DIR"); + if (StringUtils.isBlank(configDir)) configDir = "."; + log.debug("BrokerClient: config-dir: {}", configDir); + String configPropFile = configDir + "/" + "gr.iccs.imu.ems.brokerclient.properties"; + log.debug("BrokerClient: config-file: {}", configPropFile); + + // load properties + Properties p = new Properties(); + File cfgFile = Paths.get(configPropFile).toFile(); + if (cfgFile.exists() && cfgFile.isFile()) { + //ClassLoader loader = Thread.currentThread().getContextClassLoader(); + //try (java.io.InputStream in = loader.getClass().getResourceAsStream(configPropFile)) { p.load(in); } + try (java.io.InputStream in = new java.io.FileInputStream(configPropFile)) { + log.debug("BrokerClient: Loading config-properties from file: {}", configPropFile); + p.load(in); + } + log.debug("BrokerClient: config-properties: {}", p); + log.info("BrokerClient: Configuration loaded from file: {}", configPropFile); + } else { + log.debug("BrokerClient: Config file not found or is not a file: {}", configPropFile); + log.info("BrokerClient: No configuration file found"); + } + + // initialize broker client + BrokerClient client = new BrokerClient(p, PasswordUtil.getInstance()); + log.info("BrokerClient: Default Configuration:\n{}", client.properties); + + return client; + } + + public static BrokerClient newClient(String username, String password) throws java.io.IOException, JMSException { + BrokerClient client = newClient(); + if (username!=null && password!=null) { + client.getClientProperties().setBrokerUsername(username); + client.getClientProperties().setBrokerPassword(password); + } + return client; + } + + // ------------------------------------------------------------------------ + + public BrokerClientProperties getClientProperties() { + checkProperties(); + return properties; + } + + protected void checkProperties() { + if (properties==null) { + //use defaults + properties = new BrokerClientProperties(); + } + } + + // ------------------------------------------------------------------------ + + public synchronized Set getDestinationNames(String connectionString) throws JMSException { + // open or reuse connection + checkProperties(); + boolean _closeConn = false; + if (session==null) { + openConnection(connectionString); + _closeConn = ! properties.isPreserveConnection(); + } + + // Get destinations from Broker + log.info("BrokerClient.getDestinationNames(): Getting destinations: connection={}, username={}", connectionString, properties.getBrokerUsername()); + ActiveMQConnection conn = (ActiveMQConnection)connection; + DestinationSource ds = conn.getDestinationSource(); + Set queues = ds.getQueues(); + Set topics = ds.getTopics(); + Set tempQueues = ds.getTemporaryQueues(); + Set tempTopics = ds.getTemporaryTopics(); + log.info("BrokerClient.getDestinationNames(): Getting destinations: done"); + + // Get destination names + HashSet destinationNames = new HashSet<>(); + for (ActiveMQQueue q : queues) destinationNames.add("QUEUE "+q.getQueueName()); + for (ActiveMQTopic t : topics) destinationNames.add("TOPIC "+t.getTopicName()); + for (ActiveMQTempQueue tq : tempQueues) destinationNames.add("Temp QUEUE "+tq.getQueueName()); + for (ActiveMQTempTopic tt : tempTopics) destinationNames.add("Temp TOPIC "+tt.getTopicName()); + + // close connection + if (_closeConn) { + closeConnection(); + } + + return destinationNames; + } + + // ------------------------------------------------------------------------ + + public enum MESSAGE_TYPE { TEXT, OBJECT, BYTES, MAP }; + + public synchronized void publishEvent(String connectionString, String destinationName, Map eventMap) throws JMSException { + _publishEvent(connectionString, destinationName, MESSAGE_TYPE.TEXT, new EventMap(eventMap), null); + } + + public synchronized void publishEvent(String connectionString, String destinationName, Map eventMap, Map propertiesMap) throws JMSException { + _publishEvent(connectionString, destinationName, MESSAGE_TYPE.TEXT, new EventMap(eventMap), propertiesMap); + } + + public synchronized void publishEvent(String connectionString, String destinationName, String eventContents) throws JMSException { + _publishEvent(connectionString, destinationName, MESSAGE_TYPE.TEXT, eventContents, null); + } + + public synchronized void publishEvent(String connectionString, String destinationName, String eventContents, Map propertiesMap) throws JMSException { + _publishEvent(connectionString, destinationName, MESSAGE_TYPE.TEXT, eventContents, propertiesMap); + } + + public synchronized void publishEvent(String connectionString, String destinationName, String type, Serializable eventContents, Map propertiesMap) throws JMSException { + MESSAGE_TYPE messageType = StringUtils.isNotBlank(type) + ? MESSAGE_TYPE.valueOf(type.trim().toUpperCase()) + : MESSAGE_TYPE.TEXT; + _publishEvent(connectionString, destinationName, messageType, eventContents, propertiesMap); + } + + public synchronized void publishEventWithCredentials(String connectionString, String username, String password, String destinationName, Map eventMap) throws JMSException { + _publishEvent(connectionString, username, password, destinationName, MESSAGE_TYPE.TEXT, new EventMap(eventMap), null); + } + + public synchronized void publishEventWithCredentials(String connectionString, String username, String password, String destinationName, Map eventMap, Map propertiesMap) throws JMSException { + _publishEvent(connectionString, username, password, destinationName, MESSAGE_TYPE.TEXT, new EventMap(eventMap), propertiesMap); + } + + public synchronized void publishEventWithCredentials(String connectionString, String username, String password, String destinationName, String eventContents) throws JMSException { + _publishEvent(connectionString, username, password, destinationName, MESSAGE_TYPE.TEXT, eventContents, null); + } + + public synchronized void publishEventWithCredentials(String connectionString, String username, String password, String destinationName, String eventContents, Map propertiesMap) throws JMSException { + _publishEvent(connectionString, username, password, destinationName, MESSAGE_TYPE.TEXT, eventContents, propertiesMap); + } + + public synchronized void publishEventWithCredentials(String connectionString, String username, String password, String destinationName, String type, Serializable eventContents, Map propertiesMap) throws JMSException { + MESSAGE_TYPE messageType = StringUtils.isNotBlank(type) + ? MESSAGE_TYPE.valueOf(type.trim().toUpperCase()) + : MESSAGE_TYPE.TEXT; + _publishEvent(connectionString, username, password, destinationName, messageType, eventContents, propertiesMap); + } + + protected synchronized void _publishEvent(String connectionString, String destinationName, MESSAGE_TYPE messageType, Serializable event, Map propertiesMap) throws JMSException { + _publishEvent(connectionString, null, null, destinationName, messageType, event, propertiesMap); + } + + @SneakyThrows + protected synchronized void _publishEvent(String connectionString, String username, String password, String destinationName, MESSAGE_TYPE messageType, Serializable event, Map propertiesMap) throws JMSException { + // open or reuse connection + checkProperties(); + boolean _closeConn = false; + if (session==null) { + if (StringUtils.isBlank(username)) + openConnection(connectionString); + else + openConnection(connectionString, username, password); + _closeConn = ! properties.isPreserveConnection(); + } + + // Create the destination (Topic or Queue) + //Destination destination = session.createQueue( destinationName ); + Destination destination = session.createTopic(destinationName); + + // Create a MessageProducer from the Session to the Topic or Queue + MessageProducer producer = session.createProducer(destination); + producer.setDeliveryMode(javax.jms.DeliveryMode.NON_PERSISTENT); + + // Create a messages + String payloadText = null; + Message message; + switch (messageType) { + case MAP: + if (event instanceof Map) { + final MapMessage mapMsg = session.createMapMessage(); + for (Object key : ((Map) event).keySet()) { + Object val = ((Map) event).get(key); + String k = key != null ? key.toString() : null; + mapMsg.setObject(k, val); + } + payloadText = gson.toJson(event); + message = mapMsg; + break; + } else { + log.warn("BrokerClient.publishEvent(): Payload is not a Map: {}", event.getClass().getName()); + log.warn("BrokerClient.publishEvent(): Will send an Object message"); + messageType = MESSAGE_TYPE.OBJECT; + } + case OBJECT: + payloadText = (event instanceof Map) + ? gson.toJson(event) + : event.toString(); + message = session.createObjectMessage(event); + break; + case BYTES: + byte[] bytesArr; + if (event instanceof byte[]) { + bytesArr = (byte[]) event; + } else { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bos)) + { + out.writeObject(event); + bytesArr = bos.toByteArray(); + } + } + BytesMessage bytesMsg = session.createBytesMessage(); + bytesMsg.writeBytes(bytesArr); + payloadText = new String(bytesArr); + message = bytesMsg; + break; + case TEXT: + default: + payloadText = event instanceof Map + ? gson.toJson(event) + : event.toString(); + message = session.createTextMessage(payloadText); + break; + } + log.debug("BrokerClient.publishEvent(): Message payload: payload={}", payloadText); + + if (propertiesMap!=null) + for (Map.Entry e : propertiesMap.entrySet()) + if (StringUtils.isNotBlank(e.getKey())) + message.setStringProperty(e.getKey(), e.getValue()); + + // Tell the producer to send the message + long hash = message.hashCode(); + log.debug("BrokerClient.publishEvent(): Sending {} message: connection={}, username={}, destination={}, hash={}, payload={}, properties={}", messageType, connectionString, properties.getBrokerUsername(), destinationName, hash, event, propertiesMap); + producer.send(message); + log.debug("BrokerClient.publishEvent(): {} message sent: connection={}, username={}, destination={}, hash={}, payload={}, properties={}", messageType, connectionString, properties.getBrokerUsername(), destinationName, hash, event, propertiesMap); + + // close connection + if (_closeConn) { + closeConnection(); + } + } + + // ------------------------------------------------------------------------ + + public void subscribe(String connectionString, String destinationName, MessageListener listener) throws JMSException { + // Create or open connection + checkProperties(); + if (session==null) { + openConnection(connectionString); + } + + // Create the destination (Topic or Queue) + log.info("BrokerClient: Subscribing to destination: {}...", destinationName); + //Destination destination = session.createQueue( destinationName ); + Destination destination = session.createTopic(destinationName); + + // Create a MessageConsumer from the Session to the Topic or Queue + MessageConsumer consumer = session.createConsumer(destination); + consumer.setMessageListener(listener); + listeners.put(listener, consumer); + } + + public void unsubscribe(MessageListener listener) throws JMSException { + MessageConsumer consumer = listeners.get(listener); + if (consumer!=null) { + consumer.close(); + } + } + + // ------------------------------------------------------------------------ + + public enum ON_EXCEPTION { IGNORE, LOG_AND_IGNORE, THROW, LOG_AND_THROW } + + public void receiveEvents(String connectionString, String destinationName, MessageListener listener) throws JMSException { + receiveEvents(connectionString, destinationName, listener, ON_EXCEPTION.LOG_AND_IGNORE); + } + + public void receiveEvents(String connectionString, String destinationName, MessageListener listener, ON_EXCEPTION onException) throws JMSException { + checkProperties(); + MessageConsumer consumer = null; + boolean _closeConn = false; + try { + // Create or open connection + if (session==null) { + openConnection(connectionString); + _closeConn = ! properties.isPreserveConnection(); + } + + // Create the destination (Topic or Queue) + log.info("BrokerClient: Subscribing to destination: {}...", destinationName); + //Destination destination = session.createQueue( destinationName ); + Destination destination = session.createTopic(destinationName); + + // Create a MessageConsumer from the Session to the Topic or Queue + consumer = session.createConsumer(destination); + + // Wait for messages + boolean logException = onException==ON_EXCEPTION.LOG_AND_IGNORE || onException==ON_EXCEPTION.LOG_AND_THROW; + boolean throwException = onException==ON_EXCEPTION.THROW || onException==ON_EXCEPTION.LOG_AND_THROW; + log.info("BrokerClient: Waiting for messages..."); + while (true) { + Message message = consumer.receive(); + try { + listener.onMessage(message); + } catch (Exception e) { + if (logException) { + if (log.isDebugEnabled()) + log.debug("BrokerClient: Exception in callback listener: {}: {}\nevent: {}\nException: ", + e.getClass().getName(), e.getMessage(), message, e); + else + log.warn("BrokerClient: Exception in callback listener: {}: {}\nevent: {}", + e.getClass().getName(), e.getMessage(), message); + } + if (throwException) + throw e; + } + } + + } finally { + // Clean up + log.info("BrokerClient: Closing connection..."); + if (consumer != null) consumer.close(); + if (_closeConn) { + closeConnection(); + } + } + } + + // ------------------------------------------------------------------------ + + public ActiveMQConnectionFactory createConnectionFactory() { + // Create connection factory based on Broker URL scheme + checkProperties(); + final ActiveMQConnectionFactory connectionFactory; + String brokerUrl = properties.getBrokerUrl(); + if (brokerUrl.startsWith("ssl")) { + log.debug("BrokerClient.createConnectionFactory(): Creating new SSL connection factory instance: url={}", brokerUrl); + final ActiveMQSslConnectionFactory sslConnectionFactory = new ActiveMQSslConnectionFactory(brokerUrl); + try { + sslConnectionFactory.setTrustStore(properties.getSsl().getTruststoreFile()); + sslConnectionFactory.setTrustStoreType(properties.getSsl().getTruststoreType()); + sslConnectionFactory.setTrustStorePassword(properties.getSsl().getTruststorePassword()); + sslConnectionFactory.setKeyStore(properties.getSsl().getKeystoreFile()); + sslConnectionFactory.setKeyStoreType(properties.getSsl().getKeystoreType()); + sslConnectionFactory.setKeyStorePassword(properties.getSsl().getKeystorePassword()); + //sslConnectionFactory.setKeyStoreKeyPassword( properties........ ); + + connectionFactory = sslConnectionFactory; + } catch (final Exception theException) { + throw new Error(theException); + } + } else { + log.debug("BrokerClient.createConnectionFactory(): Creating new non-SSL connection factory instance: url={}", brokerUrl); + connectionFactory = new ActiveMQConnectionFactory(brokerUrl); + } + + // Other connection factory settings + //connectionFactory.setSendTimeout(....5000L); + //connectionFactory.setTrustedPackages(Arrays.asList("gr.iccs.imu.ems")); + connectionFactory.setTrustAllPackages(true); + connectionFactory.setWatchTopicAdvisories(true); + + return connectionFactory; + } + + // ------------------------------------------------------------------------ + + public synchronized void openConnection() throws JMSException { + checkProperties(); + openConnection(properties.getBrokerUrl(), null, null); + } + + public synchronized void openConnection(String connectionString) throws JMSException { + openConnection(connectionString, null, null); + } + + public synchronized void openConnection(String connectionString, String username, String password) throws JMSException { + openConnection(connectionString, username, password, properties.isPreserveConnection()); + } + + public synchronized void openConnection(String connectionString, String username, String password, boolean preserveConnection) throws JMSException { + checkProperties(); + if (connectionString == null) connectionString = properties.getBrokerUrl(); + log.debug("BrokerClient: Credentials provided as arguments: username={}, password={}", username, passwordUtil.encodePassword(password)); + if (StringUtils.isBlank(username)) { + username = properties.getBrokerUsername(); + password = properties.getBrokerPassword(); + log.debug("BrokerClient: Credentials read from properties: username={}, password={}", username, passwordUtil.encodePassword(password)); + } + + // Create connection factory + ActiveMQConnectionFactory connectionFactory = createConnectionFactory(); + connectionFactory.setBrokerURL(connectionString); + if (StringUtils.isNotBlank(username) && password != null) { + connectionFactory.setUserName(username); + connectionFactory.setPassword(password); + } + log.debug("BrokerClient: Connection credentials: username={}, password={}", username, passwordUtil.encodePassword(password)); + + // Create a Connection + log.debug("BrokerClient: Connecting to broker: {}...", connectionString); + Connection connection = connectionFactory.createConnection(); + connection.start(); + + // Create a Session + log.debug("BrokerClient: Opening session..."); + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + this.connection = connection; + this.session = session; + } + + public synchronized void closeConnection() throws JMSException { + // Clean up + session.close(); + connection.close(); + session = null; + connection = null; + } +} \ No newline at end of file diff --git a/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/BrokerClientApp.java b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/BrokerClientApp.java new file mode 100644 index 0000000..7f177ef --- /dev/null +++ b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/BrokerClientApp.java @@ -0,0 +1,761 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokerclient; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.google.gson.Gson; +import gr.iccs.imu.ems.brokerclient.event.EventGenerator; +import gr.iccs.imu.ems.brokerclient.event.EventMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.command.*; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.lang3.StringUtils; + +import javax.jms.Message; +import javax.jms.Queue; +import javax.jms.*; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.io.*; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Slf4j +public class BrokerClientApp { + + private static boolean filterAMQMessages = true; + private static boolean isRecording = false; + private static File recordFile; + private static Writer recordWriter; + private static RECORD_FORMAT recordFormat; + private static CSVPrinter csvPrinter; + private static JsonGenerator jsonGenerator; + private static long playbackInterval = -1; + private static long playbackDelay = -1; + private static double playbackSpeed = 1.0; + private static Gson gson = new Gson(); + + private enum RECORD_FORMAT { CSV, JSON } + + public static void main(String args[]) throws java.io.IOException, JMSException, ScriptException { + log.info("Broker Client for EMS, v.{}", BrokerClientApp.class.getPackage().getImplementationVersion()); + if (args.length==0) { + usage(); + return; + } + + int aa=0; + String command = args[aa++]; + + filterAMQMessages = args.length>aa && args[aa].startsWith("-Q") ? false : true; + if (!filterAMQMessages) aa++; + + String username = args.length>aa && args[aa].startsWith("-U") ? args[aa++].substring(2) : null; + String password = username!=null && args.length>aa && args[aa].startsWith("-P") ? args[aa++].substring(2) : null; + if (StringUtils.isNotBlank(username) && password == null) { + password = new String(System.console().readPassword("Enter broker password: ")); + } + + if ("record".equalsIgnoreCase(command)) { + isRecording = true; + command = "receive"; + } + + // list destinations + if ("list".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + log.info("BrokerClientApp: Listing destinations:"); + BrokerClient client = BrokerClient.newClient(username, password); + client.getDestinationNames(url).stream().forEach(d -> log.info(" {}", d)); + } else + // send an event + if ("publish".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + String topic = args[aa++]; + String type = args[aa].startsWith("-T") ? args[aa++].substring(2) : "text"; + String value = args[aa++]; + String level = args[aa++]; + EventMap event = new EventMap(Double.parseDouble(value), Integer.parseInt(level), System.currentTimeMillis()); + sendEvent(url, username, password, topic, type, event, collectProperties(args, aa)); + } else + if ("publish2".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + String topic = args[aa++]; + String type = args[aa].startsWith("-T") ? args[aa++].substring(2) : "text"; + String payload = args[aa++]; + payload = payload + .replaceAll("%TIMESTAMP%|%TS%", ""+System.currentTimeMillis()); + EventMap event = gson.fromJson(payload, EventMap.class); + sendEvent(url, username, password, topic, type, event, collectProperties(args, aa)); + } else + if ("publish3".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + String topic = args[aa++]; + String type = args[aa].startsWith("-T") ? args[aa++].substring(2) : "text"; + String payload = args[aa++]; + payload = payload + .replaceAll("%TIMESTAMP%|%TS%", ""+System.currentTimeMillis()); + Map properties = collectProperties(args, aa); + if ("map".equalsIgnoreCase(type)) { + EventMap event = gson.fromJson(payload, EventMap.class); + sendEvent(url, username, password, topic, type, event, properties); + } else { + sendEvent(url, username, password, topic, type, payload, properties); + } + } else + // receive events from topic + if ("receive".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + String topic = args[aa++]; + + if (isRecording) + initRecording(args, aa); + + log.info("BrokerClientApp: Subscribing to topic: {}", topic); + BrokerClient client = BrokerClient.newClient(username, password); + client.receiveEvents(url, topic, getMessageListener()); + } else + // playback events + if ("playback".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + initPlayback(args, aa); + playbackEvents(url, username, password); + } else + // subscribe to topic + if ("subscribe".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + String topic = args[aa++]; + log.info("BrokerClientApp: Subscribing to topic: {}", topic); + BrokerClient client = BrokerClient.newClient(username, password); + MessageListener listener = null; + client.subscribe(url, topic, listener = getMessageListener()); + + log.info("BrokerClientApp: Hit ENTER to exit"); + try { + System.in.read(); + } catch (Exception e) {} + log.info("BrokerClientApp: Closing connection..."); + + client.unsubscribe(listener); + client.closeConnection(); + log.info("BrokerClientApp: Exiting..."); + + } else + // start event generator + if ("generator".equalsIgnoreCase(command)) { + String url = processUrlArg( args[aa++] ); + String topic = args[aa++]; + long interval = Long.parseLong(args[aa++]); + long howmany = Long.parseLong(args[aa++]); + double lowerValue = Double.parseDouble(args[aa++]); + double upperValue = Double.parseDouble(args[aa++]); + int level = Integer.parseInt(args[aa++]); + + BrokerClient client = BrokerClient.newClient(); + client.openConnection(url, username, password, true); + EventGenerator generator = new EventGenerator(client); + //generator.setClient(client); + generator.setBrokerUrl(url); + generator.setDestinationName(topic); + generator.setInterval(interval); + generator.setHowMany(howmany); + generator.setLowerValue(lowerValue); + generator.setUpperValue(upperValue); + generator.setLevel(level); + generator.run(); + client.closeConnection(); + } else + // Run JS script + if ("js".equalsIgnoreCase(command)) { + ScriptEngineManager manager = new ScriptEngineManager(); + String engineName = "nashorn"; + if (aa{ + log.info(" Engine: {} {}, {}, Language: {} {}, Mime: {}, Ext: {}", + s.getEngineName(), s.getEngineVersion(), s.getNames(), + s.getLanguageName(), s.getLanguageVersion(), + s.getMimeTypes(), s.getExtensions()); + }); + } + aa++; + } + + ScriptEngine engine = manager.getEngineByName(engineName); + Bindings bindings = engine.createBindings(); + String scriptFile = args[aa++]; + + ArrayList jsArgs = new ArrayList<>(); + for (; aa collectProperties(String[] args, int aa) { + return Arrays.stream(args, aa, args.length) + .map(s->s.split("[=:]",2)) + .filter(p->StringUtils.isNotBlank(p[0])) + .collect(Collectors.toMap( + p->p[0].trim(), + p->p.length>1 ? p[1] : "" + )); + } + + private static String processUrlArg(String url) { + url = url.replace("%KAP%", "daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true"); + log.debug("BrokerClientApp: Effective URL: {}", url); + return url; + } + + private static void sendEvent(String url, String username, String password, String topic, String type, Serializable payload, Map properties) throws JMSException, IOException { + log.info("BrokerClientApp: Publishing event: {}", payload); + BrokerClient client = BrokerClient.newClient(username, password); + client.publishEvent(url, topic, type, payload, properties); + log.info("BrokerClientApp: Event payload: {}", payload); + } + + private static MessageListener getMessageListener() { + return message -> { + try { + // get message destination + String destinationName = getDestinationName(message); + + // filter out Advisory messages + if (filterAMQMessages && StringUtils.startsWithIgnoreCase(destinationName, "ActiveMQ.")) { + log.trace("BrokerClientApp: - {}: ActiveMQ message filtered out: {}", destinationName, message); + log.debug("AMQ: {}:\n{}", destinationName, message); + return; + } + + // get properties as string + String properties; + if (message instanceof ActiveMQMessage) { + try { + ActiveMQMessage amqMessage = (ActiveMQMessage) message; + properties = amqMessage.getProperties() + .entrySet().stream() + .map(x -> x.getKey() + "=" + x.getValue()) + .collect(Collectors.joining(",", "{", "}")); + } catch (Exception e) { + properties = "ERROR "+e.getMessage(); + log.error("BrokerClientApp: - {}: ERROR while reading properties: ", destinationName, e); + } + } else { + //properties = "Not an ActiveMQ message"; + Enumeration en = message.getPropertyNames(); + Map pMap = new HashMap<>(); + while (en.hasMoreElements()) { + String pName = en.nextElement().toString(); + Object pVal = message.getObjectProperty(pName); + if (pVal!=null) + pMap.put(pName, pVal.toString()); + else + pMap.put(pName, null); + } + properties = pMap.toString(); + } + + // print message body and info + if (message instanceof ObjectMessage) { + ObjectMessage objMessage = (ObjectMessage) message; + Object obj = objMessage.getObject(); + String objClass = obj!=null ? obj.getClass().getName() : null; + log.trace("BrokerClientApp: - {}: Received object message: {}: {}", destinationName, objClass, obj); + log.info("OBJ: {}: properties: {}\n{}: {}", destinationName, properties, objClass, obj); + } else if (message instanceof MapMessage) { + MapMessage mapMessage = (MapMessage) message; + Enumeration en = mapMessage.getMapNames(); + Map map = new HashMap<>(); + while (en.hasMoreElements()) { + String k = en.nextElement().toString(); + map.put(k, mapMessage.getObject(k)); + } + log.trace("BrokerClientApp: - {}: Received map message: {}", destinationName, map); + log.info("MAP: {}: properties: {}\n{}", destinationName, properties, map); + } else if (message instanceof BytesMessage) { + BytesMessage bytesMessage = (BytesMessage) message; + byte[] bytes = new byte[(int)bytesMessage.getBodyLength()]; + bytesMessage.readBytes(bytes); + //String str = new String(bytes); + Object obj; + try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream is = new ObjectInputStream(bis)) + { + obj = is.readObject(); + } catch (Exception e) { + obj = bytes; + } + log.trace("BrokerClientApp: - {}: Received bytes message: {}", destinationName, bytes); + log.info("BYTES: {}: properties: {}\n{}\n{}", destinationName, properties, bytes, obj); + } else if (message instanceof TextMessage) { + TextMessage textMessage = (TextMessage) message; + String text = textMessage.getText(); + log.trace("BrokerClientApp: - {}: Received text message: {}", destinationName, text); + log.info("TXT: {}: properties: {}\n{}", destinationName, properties, text); + } else { + log.trace("BrokerClientApp: - {}: Received message: {}", destinationName, message); + log.info("MSG: {}: properties: {}\n{}", destinationName, properties, message); + } + + // record message to file + recordEvent(message); + + } catch (JMSException je) { + log.warn("BrokerClientApp: onMessage: EXCEPTION: ", je); + } + }; + } + + private static int initRecording(String[] args, int aa) throws IOException { + // Process recording command line arguments + String format = null; + if (args[aa].startsWith("-M")) + format = args[aa++].substring(2).toLowerCase(); + String fileName = args[aa++]; + File file = Paths.get(fileName).toFile(); + String ext = StringUtils.substringAfterLast(file.getName(), "."); + if (StringUtils.isNotBlank(format)) { + if (!("csv".equalsIgnoreCase(format) || "json".equalsIgnoreCase(format))) + throw new IllegalArgumentException("Unsupported recording format: "+format); + else if ("csv".equalsIgnoreCase(format)) recordFormat = RECORD_FORMAT.CSV; + else if ("json".equalsIgnoreCase(format)) recordFormat = RECORD_FORMAT.JSON; + } + else if ("csv".equalsIgnoreCase(ext)) recordFormat = RECORD_FORMAT.CSV; + else if ("txt".equalsIgnoreCase(ext)) recordFormat = RECORD_FORMAT.CSV; + else if ("json".equalsIgnoreCase(ext)) recordFormat = RECORD_FORMAT.JSON; + else { + log.warn("Unknown file extension. Assuming CSV"); + recordFormat = RECORD_FORMAT.CSV; + } + recordFile = file; + + // Initialize recording + log.info("Record format: {}", recordFormat); + log.info("Record file: {}", recordFile); + log.info("Start recording..."); + + recordWriter = new BufferedWriter(new FileWriter(file)); + if (recordFormat==RECORD_FORMAT.CSV) { + csvPrinter = new CSVPrinter(recordWriter, CSVFormat.DEFAULT + .withHeader("Timestamp", "Destination", "Mime", "Type", "Contents", "Properties")); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { csvPrinter.close(true); recordWriter.close(); } catch (IOException e) { log.error("BrokerClientApp: EXCEPTION while closing record file: ", e); } + log.info("Recording stopped"); + })); + } else + if (recordFormat==RECORD_FORMAT.JSON) { + jsonGenerator = new JsonFactory() + .createGenerator(recordWriter) + .setPrettyPrinter(new DefaultPrettyPrinter()); + jsonGenerator.writeStartArray(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { jsonGenerator.writeEndArray(); jsonGenerator.close(); recordWriter.close(); } catch (IOException e) { log.error("BrokerClientApp: EXCEPTION while closing record file: ", e); } + log.info("Recording stopped"); + })); + } else + throw new IllegalArgumentException("Unsupported recording format: "+recordFormat); + + return aa; + } + + private static void recordEvent(Message message) { + if (!isRecording) return; + + try { + if (!(message instanceof ActiveMQMessage)) { + throw new IllegalArgumentException("Unsupported Message type: "+message.getClass().getName()); + } + + ActiveMQMessage amqMessage = (ActiveMQMessage) message; + long timestamp = message.getJMSTimestamp(); + String destinationName = getDestinationName(message); + String mime = amqMessage.getJMSXMimeType(); + String type; + + String content; + if (amqMessage instanceof ActiveMQTextMessage) { + type = BrokerClient.MESSAGE_TYPE.TEXT.name(); + content = ((ActiveMQTextMessage)amqMessage).getText(); + } else + if (amqMessage instanceof ActiveMQObjectMessage) { + type = BrokerClient.MESSAGE_TYPE.OBJECT.name(); + Object obj = ((ActiveMQObjectMessage)amqMessage).getObject(); + /*String objClass = obj!=null ? obj.getClass().getName() : null; + content = objClass + ":" + obj.toString();*/ + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) + { + oos.writeObject(obj); + byte[] bytes = baos.toByteArray(); + content = Base64.getEncoder().encodeToString(bytes); + } + } else + if (amqMessage instanceof ActiveMQMapMessage) { + type = BrokerClient.MESSAGE_TYPE.MAP.name(); + /*content = ((ActiveMQMapMessage)amqMessage).getContentMap() + .entrySet().stream() + .map(x -> x.getKey() + "=" + x.getValue()) + .collect(Collectors.joining(",", "{", "}"));*/ + content = gson.toJson(((ActiveMQMapMessage)amqMessage).getContentMap()); + } else + if (amqMessage instanceof ActiveMQBytesMessage) { + type = BrokerClient.MESSAGE_TYPE.BYTES.name(); + byte[] bytes = amqMessage.getContent().getData(); + content = Base64.getEncoder().encodeToString(bytes); + } else { + type = BrokerClient.MESSAGE_TYPE.BYTES.name(); + byte[] bytes = amqMessage.getContent().getData(); + content = Base64.getEncoder().encodeToString(bytes); + } + + String properties = amqMessage.getProperties() + .entrySet().stream() + .map(x -> x.getKey() + "=" + x.getValue()) + .collect(Collectors.joining(",", "{", "}")); + + log.trace("REC> timestamp={}, topic={}, mime={}, type={}, contents={}, properties={}", timestamp, destinationName, mime, type, content, properties); + if (recordFormat==RECORD_FORMAT.CSV) { + csvPrinter.printRecord(timestamp, destinationName, mime, type, content, properties); + csvPrinter.flush(); + } else + if (recordFormat==RECORD_FORMAT.JSON) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeNumberField("timestamp", timestamp); + jsonGenerator.writeStringField("destination", destinationName); + jsonGenerator.writeStringField("mime", mime); + jsonGenerator.writeStringField("type", type); + jsonGenerator.writeStringField("content", content); + jsonGenerator.writeStringField("properties", properties); + jsonGenerator.writeEndObject(); + jsonGenerator.flush(); + } + + } catch (Exception e) { + log.error("BrokerClientApp: EXCEPTION during RECORDING: ", e); + } + } + + private static int initPlayback(String[] args, int aa) throws IOException { + // Process recording command line arguments + playbackInterval = -1L; + playbackDelay = -1L; + int startAa = aa; + if (args[aa].startsWith("-I")) { + playbackInterval = Long.parseLong(args[aa++].substring(2).toLowerCase()); + if (playbackInterval<0) throw new IllegalArgumentException("Playback Interval cannot be negative: "+playbackInterval); + } + if (args[aa].startsWith("-D")) { + playbackDelay = Long.parseLong(args[aa++].substring(2).toLowerCase()); + if (playbackDelay<0) throw new IllegalArgumentException("Playback Delay cannot be negative: "+playbackDelay); + } + if (args[aa].startsWith("-S")) { + playbackSpeed = Double.parseDouble(args[aa++].substring(2).toLowerCase()); + if (playbackSpeed<=0) throw new IllegalArgumentException("Playback Speed cannot be negative or zero: "+playbackSpeed); + } + if (aa-startAa>1) + throw new IllegalArgumentException("You cannot use -I, -D, -S switches at the same time"); + + String format = null; + if (args[aa].startsWith("-M")) + format = args[aa++].substring(2).toLowerCase(); + String fileName = args[aa++]; + File file = Paths.get(fileName).toFile(); + String ext = StringUtils.substringAfterLast(file.getName(), "."); + if (StringUtils.isNotBlank(format)) { + if (!("csv".equalsIgnoreCase(format) || "json".equalsIgnoreCase(format))) + throw new IllegalArgumentException("Unsupported recording format: "+format); + else if ("csv".equalsIgnoreCase(format)) recordFormat = RECORD_FORMAT.CSV; + else if ("json".equalsIgnoreCase(format)) recordFormat = RECORD_FORMAT.JSON; + } + else if ("csv".equalsIgnoreCase(ext)) recordFormat = RECORD_FORMAT.CSV; + else if ("txt".equalsIgnoreCase(ext)) recordFormat = RECORD_FORMAT.CSV; + else if ("json".equalsIgnoreCase(ext)) recordFormat = RECORD_FORMAT.JSON; + else { + log.warn("Unknown file extension. Assuming CSV"); + recordFormat = RECORD_FORMAT.CSV; + } + recordFile = file; + + // Initialize recording + log.info("Playback format: {}", recordFormat); + log.info("Playback file: {}", recordFile); + + return aa; + } + + private static long playbackEvents(String url, String username, String password) throws IOException, JMSException { + AtomicLong countSuccess = new AtomicLong(); + AtomicLong countFail = new AtomicLong(); + + BrokerClient client = BrokerClient.newClient(); + client.openConnection(url, username, password, true); + + boolean useInterval = (playbackInterval>=0); + boolean useDelay = (playbackDelay>=0); + + log.info("Start playback..."); + long startTm = System.currentTimeMillis(); + final long[] prevValues = {-1L, -1L, -1L}; // Previous Event Timestamp, Previous System time, Last sleep time + + if (recordFormat==RECORD_FORMAT.CSV) + playbackEventsFromCsv(client, prevValues, useInterval, useDelay, countSuccess, countFail, url); + else if (recordFormat==RECORD_FORMAT.JSON) + playbackEventsFromJson(client, prevValues, useInterval, useDelay, countSuccess, countFail, url); + else + throw new IllegalArgumentException("Unsupported or missing recording format: "+recordFormat); + + long endTm = System.currentTimeMillis(); + long count = countSuccess.get() + countFail.get(); + + client.closeConnection(); + + printPlaybackStatistics(endTm - startTm, countSuccess, countFail); + + return count; + } + + private static void playbackEventsFromCsv(BrokerClient client, long[] prevValues, boolean useInterval, boolean useDelay, + AtomicLong countSuccess, AtomicLong countFail, String url) + throws IOException, JMSException + { + CSVFormat.DEFAULT + .withFirstRecordAsHeader() + .parse(new BufferedReader(new FileReader(recordFile))) + .forEach(rec -> { + // read event data + long timestamp = Long.parseLong(rec.get("Timestamp")); + String destinationName = rec.get("Destination"); + String mime = rec.get("Mime"); + String type = rec.get("Type"); + String contents = rec.get("Contents"); + String properties = rec.get("Properties"); + + log.trace("REPLAY> Event data: timestamp={}, destination={}, mime={}, type={}, content={}, properties={}", + timestamp, destinationName, mime, type, contents, properties); + + // read event properties + if (properties.startsWith("{") && properties.endsWith("}")) + properties = properties.substring(1, properties.length()-1); + Map propertiesMap = Arrays.stream(properties.split(",")) + .filter(StringUtils::isNotBlank) + .map(p -> p.split("=",2)) + .collect(Collectors.toMap(p->p[0], p->p.length>1 ? p[1] : "")); + + // wait and send + try { + waitAndSend(client, prevValues, useInterval, useDelay, url, + timestamp, destinationName, type, contents, propertiesMap, countSuccess, countFail); + } catch (Exception e) { + log.error("REPLAY> EXCEPTION: Ignoring record entry: timestamp={}, destination={}, mime={}, type={}, content={}, properties={}\n", + timestamp, destinationName, mime, type, contents, properties, e); + } + }); + } + + private static void playbackEventsFromJson(BrokerClient client, long[] prevValues, boolean useInterval, boolean useDelay, + AtomicLong countSuccess, AtomicLong countFail, String url) + throws JMSException, IOException + { + Reader playbackReader = new BufferedReader(new FileReader(recordFile)); + JsonParser jsonParser = new JsonFactory().createParser(playbackReader); + + if (jsonParser.nextToken() == JsonToken.START_ARRAY) { + while (jsonParser.nextToken() == JsonToken.START_OBJECT) { + // read event data + long timestamp = -1L; + String destinationName = null; + String mime = null; + String type = null; + String contents = null; + String properties = ""; + + while (jsonParser.nextToken() != JsonToken.END_OBJECT) { + String fieldName = jsonParser.getCurrentName(); + jsonParser.nextToken(); + if ("timestamp".equals(fieldName)) timestamp = jsonParser.getLongValue(); + else if ("destination".equals(fieldName)) destinationName = jsonParser.getText(); + else if ("mime".equals(fieldName)) mime = jsonParser.getText(); + else if ("type".equals(fieldName)) type = jsonParser.getText(); + else if ("content".equals(fieldName)) contents = jsonParser.getText(); + else if ("properties".equals(fieldName)) properties = jsonParser.getText(); + else + log.warn("REPLAY> UNKNOWN JSON field at event #{}: {}", countSuccess.get()+countFail.get()+1, fieldName); + } + + log.trace("REPLAY> Event data: timestamp={}, destination={}, mime={}, type={}, content={}, properties={}", + timestamp, destinationName, mime, type, contents, properties); + + // read event properties + if (properties.startsWith("{") && properties.endsWith("}")) + properties = properties.substring(1, properties.length()-1); + Map propertiesMap = Arrays.stream(properties.split(",")) + .map(p -> p.split("=",2)) + .collect(Collectors.toMap(p->p[0], p->p[1])); + + // wait and send + try { + waitAndSend(client, prevValues, useInterval, useDelay, url, + timestamp, destinationName, type, contents, propertiesMap, countSuccess, countFail); + } catch (Exception e) { + log.error("REPLAY> EXCEPTION: Ignoring record entry: timestamp={}, destination={}, mime={}, type={}, content={}, properties={}\n", + timestamp, destinationName, mime, type, contents, properties, e); + } + } + } + + jsonParser.close(); + playbackReader.close(); + } + + private static void waitAndSend(BrokerClient client, long[] prevValues, boolean useInterval, boolean useDelay, String url, + long timestamp, String destinationName, String type, String contents, Map propertiesMap, + AtomicLong countSuccess, AtomicLong countFail) + throws IOException, ClassNotFoundException + { + // prepare event payload + Serializable payload; + if ("TEXT".equalsIgnoreCase(type)) { + payload = contents; + } else + if ("OBJECT".equalsIgnoreCase(type)) { + /*String[] part = contents.split(":",2); + payload = part[1];*/ + byte[] bytes = Base64.getDecoder().decode(contents); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) + { + payload = (Serializable) ois.readObject(); + } + } else + if ("MAP".equalsIgnoreCase(type)) { + payload = gson.fromJson(contents, EventMap.class); + } else + if ("BYTES".equalsIgnoreCase(type)) { + payload = Base64.getDecoder().decode(contents); + } else { + //payload = contents; + payload = Base64.getDecoder().decode(contents); + } + + // calculate wait time and sleep + if (prevValues[1]>0) { + // calculate wait time + long sleepTime = 0; + long now = System.currentTimeMillis(); + if (useInterval) { + log.trace("REPLAY> Interval: now={}, prev={}, playback={}", now, prevValues[1], playbackInterval); + prevValues[1] = prevValues[1] + playbackInterval; + sleepTime = prevValues[1] - now; + log.trace("REPLAY> : sleep={}, new-prev={}", sleepTime, prevValues[1]); + } else if (useDelay) { + log.trace("REPLAY> Delay: now={}, playback={}", now, playbackDelay); + sleepTime = playbackDelay; + } else { + long diff = (long)((timestamp - prevValues[0]) / playbackSpeed); + log.trace("REPLAY> Recorded: diff={}, now={}, prev={}", diff, now, prevValues[1]); + prevValues[0] = timestamp; + prevValues[1] += diff; + sleepTime = prevValues[1] - now; + log.trace("REPLAY> : sleep={}, new-prev={}", sleepTime, prevValues[1]); + } + prevValues[2] = sleepTime; + // wait to send + try { + log.debug("REPLAY> sleep={}", sleepTime); + if (sleepTime > 1) + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + throw new RuntimeException("Playback interrupted"); + } + } else { + prevValues[0] = timestamp; + prevValues[1] = System.currentTimeMillis(); + } + + // send event + long counter = countSuccess.get()+countFail.get()+1; + try { + log.info("BrokerClientApp: Replay event #{}", counter); + log.trace("BrokerClientApp: Publishing {} event: {}", type, payload); + client.publishEvent(url, destinationName, type, payload, propertiesMap); + log.info("BrokerClientApp: Event payload: {}", payload); + countSuccess.getAndIncrement(); + } catch (Exception e) { + log.error("BrokerClientApp: EXCEPTION while playing back event #{}: ", counter, e); + countFail.getAndIncrement(); + } + } + + private static void printPlaybackStatistics(long duration, AtomicLong countSuccess, AtomicLong countFail) { + long count = countSuccess.get() + countFail.get(); + log.info("Playback completed in {}ms", duration); + log.info(" Sent: {}", countSuccess.get()); + log.info(" Failed: {}", countFail.get()); + log.info(" Total: {}", count); + log.info(" Send Rate: {}e/s", 1000d * count / (duration)); + log.info(" Mean Delay: {}s", count<=1 ? "N/A" : (duration) / 1000d / (count-1) ); + } + + private static String getDestinationName(Message message) throws JMSException { + Destination d = message.getJMSDestination(); + if (d instanceof Topic) { + return ((Topic)d).getTopicName(); + } else + if (d instanceof Queue) { + return ((Queue)d).getQueueName(); + } else + throw new IllegalArgumentException("Argument is not a JMS destination: "+d); + } + + protected static void usage() { + log.info("BrokerClientApp: Usage: "); + log.info("BrokerClientApp: client list [-U [-P "); + log.info("BrokerClientApp: client publish [ -U [-P [-T] []*"); + log.info("BrokerClientApp: client publish2 [-U [-P [-T] []*"); + log.info("BrokerClientApp: client publish3 [-U [-P [-T] []*"); + log.info("BrokerClientApp: : text, object, bytes, map"); + log.info("BrokerClientApp: : = (use quotes if needed)"); + log.info("BrokerClientApp: client receive [-U [-P "); + log.info("BrokerClientApp: client subscribe [-U [-P "); + log.info("BrokerClientApp: client generator [-U [-P "); + log.info("BrokerClientApp: client record [-U [-P [-Mcsv|-Mjson] "); + log.info("BrokerClientApp: client playback [-U [-P [-Innn|-Dnnn|-Sd[.d]] [-Mcsv|-Mjson] "); + log.info("BrokerClientApp: client js [-E] "); + log.info("BrokerClientApp: : (tcp:|ssl:)//
:[?[%KAP%][&...additional properties]*] KAP: Keep-Alive Properties "); + } +} \ No newline at end of file diff --git a/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/event/EventGenerator.java b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/event/EventGenerator.java new file mode 100644 index 0000000..bbe9c82 --- /dev/null +++ b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/event/EventGenerator.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokerclient.event; + +import gr.iccs.imu.ems.brokerclient.BrokerClient; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.concurrent.atomic.AtomicLong; + +@Slf4j +@Data +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class EventGenerator implements Runnable { + private final static AtomicLong counter = new AtomicLong(); + private final BrokerClient client; + private String brokerUrl; + private String brokerUsername; + private String brokerPassword; + private String destinationName; + private long interval; + private long howMany = -1; + private double lowerValue; + private double upperValue; + private int level; + + private transient boolean keepRunning; + + @PostConstruct + public void printCounter() { + log.info("New EventGenerator with instance number: {}", counter.getAndIncrement()); + } + + public void start() { + if (keepRunning) return; + Thread runner = new Thread(this); + runner.setDaemon(true); + runner.start(); + } + + public void stop() { + keepRunning = false; + } + + public void run() { + log.info("EventGenerator.run(): Start sending events: event-generator: {}", this); + + keepRunning = true; + double valueRangeWidth = upperValue - lowerValue; + long countSent = 0; + while (keepRunning) { + try { + double newValue = Math.random() * valueRangeWidth + lowerValue; + EventMap event = new EventMap(newValue, level, System.currentTimeMillis()); + log.info("EventGenerator.run(): Sending event #{}: {}", countSent + 1, event); + client.publishEventWithCredentials(brokerUrl, brokerUsername, brokerPassword, destinationName, event); + countSent++; + if (countSent == howMany) keepRunning = false; + log.info("EventGenerator.run(): Event sent #{}: {}", countSent, event); + } catch (Exception ex) { + log.warn("EventGenerator.run(): WHILE-EXCEPTION: ", ex); + } + // sleep for 'interval' ms + try { + if (keepRunning) { + Thread.sleep(interval); + } + } catch (InterruptedException ex) { + log.warn("EventGenerator.run(): Sleep interrupted"); + } + } + + log.info("EventGenerator.run(): Stop sending events: event-generator: {}", this); + } +} \ No newline at end of file diff --git a/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/event/EventMap.java b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/event/EventMap.java new file mode 100644 index 0000000..295242e --- /dev/null +++ b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/event/EventMap.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokerclient.event; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@Slf4j +public class EventMap extends LinkedHashMap implements Serializable { + public EventMap() { + super(); + } + + public EventMap(Map map) { + super(map); + } + + public EventMap(double metricValue, int level, long timestamp) { + put("metricValue", metricValue); + put("level", level); + put("timestamp", timestamp); + } + + public static String[] getPropertyNames() { + return new String[]{"metricValue", "level", "timestamp"}; + } + + public static Class[] getPropertyClasses() { + return new Class[]{Double.class, Integer.class, Long.class}; + } +} \ No newline at end of file diff --git a/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/properties/BrokerClientProperties.java b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/properties/BrokerClientProperties.java new file mode 100644 index 0000000..ddf4b4d --- /dev/null +++ b/ems-core/broker-client/src/main/java/gr/iccs/imu/ems/brokerclient/properties/BrokerClientProperties.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.brokerclient.properties; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "brokerclient") +@Slf4j +public class BrokerClientProperties { + private String brokerName = "broker"; + private String brokerUrl = "tcp://localhost:61616"; + private String brokerUrlProperties; + private int managementConnectorPort = -1; + private boolean preserveConnection; + + private Ssl ssl = new Ssl(); + + private String brokerUsername; + @ToString.Exclude + private String brokerPassword; + + @Data + public static class Ssl { + private boolean clientAuthRequired; + private String truststoreFile; + private String truststoreType; + @ToString.Exclude + private String truststorePassword; + private String keystoreFile; + private String keystoreType; + @ToString.Exclude + private String keystorePassword; + } + + public BrokerClientProperties() { + brokerName = "broker"; + brokerUrl = "tcp://localhost:61616"; + brokerUrlProperties = ""; + managementConnectorPort = -1; + preserveConnection = false; + + ssl = new Ssl(); + + brokerUsername = ""; + brokerPassword = ""; + } + + public BrokerClientProperties(java.util.Properties p) { + brokerName = p.getProperty("brokerclient.broker-name", "broker"); + brokerUrl = p.getProperty("brokerclient.broker-url", "tcp://localhost:61616"); + brokerUrlProperties = p.getProperty("brokerclient.broker-url-properties", ""); + managementConnectorPort = Integer.parseInt(p.getProperty("brokerclient.connector-port", "-1")); + preserveConnection = Boolean.parseBoolean(p.getProperty("brokerclient.preserve-connection", "false")); + + ssl = new Ssl(); + ssl.truststoreFile = p.getProperty("brokerclient.ssl.truststore.file", ""); + ssl.truststoreType = p.getProperty("brokerclient.ssl.truststore.type", ""); + ssl.truststorePassword = p.getProperty("brokerclient.ssl.truststore.password", ""); + ssl.keystoreFile = p.getProperty("brokerclient.ssl.keystore.file", ""); + ssl.keystoreType = p.getProperty("brokerclient.ssl.keystore.type", ""); + ssl.keystorePassword = p.getProperty("brokerclient.ssl.keystore.password", ""); + ssl.clientAuthRequired = Boolean.parseBoolean(p.getProperty("brokerclient.ssl.client-auth.required", "false")); + + brokerUsername = p.getProperty("brokerclient.broker-username", ""); + brokerPassword = p.getProperty("brokerclient.broker-password", ""); + + brokerUrlProperties = brokerUrlProperties.replace("${brokerclient.ssl.client-auth.required}", Boolean.toString(ssl.clientAuthRequired)); + } +} diff --git a/ems-core/broker-client/src/main/resources/logback.xml b/ems-core/broker-client/src/main/resources/logback.xml new file mode 100644 index 0000000..36ee6c4 --- /dev/null +++ b/ems-core/broker-client/src/main/resources/logback.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + BC> %msg%n + + + + + + + + + + + + + diff --git a/ems-core/common/pom.xml b/ems-core/common/pom.xml new file mode 100644 index 0000000..9bad112 --- /dev/null +++ b/ems-core/common/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + common + EMS - Common to EMS server and clients + + + + + gr.iccs.imu.ems + broker-cep + ${project.version} + + + + + org.springframework + spring-web + + + + + org.projectlombok + lombok + provided + + + + diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/client/SshClient.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/client/SshClient.java new file mode 100644 index 0000000..a74d24f --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/client/SshClient.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; + +public interface SshClient { + void setConfiguration(C config); + void setUseServerKeyVerifier(boolean useServerKeyVerifier); + void start() throws IOException; + void stop() throws IOException; + InputStream getIn(); + PrintStream getOut(); + PrintStream getErr(); +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/client/SshClientProperties.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/client/SshClientProperties.java new file mode 100644 index 0000000..a819c88 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/client/SshClientProperties.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.client; + +import lombok.Data; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties +@ToString(exclude = "serverPassword") +public class SshClientProperties { + private long connectTimeout = 60000; + private long authTimeout = 60000; + private long heartbeatInterval = 60000; + private long heartbeatReplyWait = heartbeatInterval; + private long execTimeout = 120000; + private long retryPeriod = 60000; + + private String clientId; + + private String serverAddress; + private int serverPort = 22; + private String serverPubkey; + private String serverPubkeyFingerprint; + private String serverPubkeyAlgorithm; + private String serverPubkeyFormat; + + private String serverUsername; + private String serverPassword; +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/AbstractEndpointCollector.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/AbstractEndpointCollector.java new file mode 100644 index 0000000..0957d0a --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/AbstractEndpointCollector.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector; + +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.common.client.SshClientProperties; +import gr.iccs.imu.ems.common.misc.EventConstant; +import gr.iccs.imu.ems.common.recovery.RecoveryConstant; +import gr.iccs.imu.ems.util.EmsConstant; +import gr.iccs.imu.ems.util.EventBus; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.TaskScheduler; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ScheduledFuture; +import java.util.stream.Collectors; + +/** + * Abstract collector: + * Collects measurements from http server endpoint + */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractEndpointCollector implements InitializingBean, Runnable, EventBus.EventConsumer { + private final static String EVENT_COLLECTION_START = "EVENT_COLLECTION_START"; + private final static String EVENT_COLLECTION_END = "EVENT_COLLECTION_END"; + private final static String EVENT_COLLECTION_ERROR = "EVENT_COLLECTION_ERROR"; + private final static String EVENT_CONN_OK = "EVENT_CONN_OK"; + private final static String EVENT_CONN_ERROR = "EVENT_CONN_ERROR"; + private final static String EVENT_NODE_OK = "EVENT_NODE_OK"; + private final static String EVENT_NODE_FAILED = "EVENT_NODE_FAILED"; + + private final static String BASE_COLLECTION_START = "_COLLECTION_START"; + private final static String BASE_COLLECTION_END = "_COLLECTION_END"; + private final static String BASE_COLLECTION_ERROR = "_COLLECTION_ERROR"; + private final static String BASE_CONN_OK = "_CONN_OK"; + private final static String BASE_CONN_ERROR = "_CONN_ERROR"; + private final static String BASE_NODE_OK = "_NODE_OK"; + private final static String BASE_NODE_FAILED = "_NODE_FAILED"; + + protected final String collectorId; + protected final AbstractEndpointCollectorProperties properties; + protected final CollectorContext collectorContext; + protected final TaskScheduler taskScheduler; + protected final EventBus eventBus; + protected final Map>, Map> nodeToNodeEventsMap = new HashMap<>(); + + protected boolean started; + protected ScheduledFuture runner; + protected List allowedTopics; + protected Map topicMap; + + protected Map errorsMap = new HashMap<>(); + protected Map> ignoredNodes = new HashMap<>(); + + protected enum COLLECTION_RESULT { IGNORED, OK, ERROR } + + @Override + public void afterPropertiesSet() { + log.debug("Collectors::{}: properties: {}", collectorId, properties); + this.allowedTopics = properties.getAllowedTopics()==null + ? null + : properties.getAllowedTopics().stream() + .map(s -> s.split(":")[0]) + .collect(Collectors.toList()); + this.topicMap = properties.getAllowedTopics()==null + ? null + : properties.getAllowedTopics().stream() + .map(s -> s.split(":", 2)) + .collect(Collectors.toMap(a -> a[0], a -> a.length>1 ? a[1]: "")); + + registerInternalEvents("ABSTRACT"); + } + + public synchronized void start() { + // check if already running + if (started) { + log.warn("Collectors::{}: Already started", collectorId); + return; + } + + // check parameters + if (properties==null || !properties.isEnable()) { + log.warn("Collectors::{}: Collector not enabled", collectorId); + return; + } + if (properties.getDelay()<0) properties.setDelay(0); + + log.debug("Collectors::{}: configuration: {}", collectorId, properties); + + // Subscribe for SELF-HEALING plugin GIVE_UP events + eventBus.subscribe(RecoveryConstant.SELF_HEALING_RECOVERY_COMPLETED, this); + eventBus.subscribe(RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP, this); + eventBus.subscribe(EventConstant.EVENT_CLIENT_CONFIG_UPDATED, this); + + // Schedule collection execution + errorsMap.clear(); + ignoredNodes.clear(); + runner = taskScheduler.scheduleWithFixedDelay(this, Duration.ofMillis(properties.getDelay())); + started = true; + + log.info("Collectors::{}: Started", collectorId); + } + + public synchronized void stop() { + if (!started) { + log.warn("Collectors::{}: Not started", collectorId); + return; + } + + // Unsubscribe from SELF-HEALING plugin GIVE_UP events + eventBus.unsubscribe(EventConstant.EVENT_CLIENT_CONFIG_UPDATED, this); + eventBus.unsubscribe(RecoveryConstant.SELF_HEALING_RECOVERY_COMPLETED, this); + eventBus.unsubscribe(RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP, this); + + // Cancel collection execution + started = false; + runner.cancel(true); + runner = null; + ignoredNodes.values().stream().filter(Objects::nonNull).forEach(task -> task.cancel(true)); + log.info("Collectors::{}: Stopped", collectorId); + } + + @Override + public void onMessage(String topic, Object message, Object sender) { + log.trace("Collectors::{}: onMessage: BEGIN: topic={}, message={}, sender={}", collectorId, topic, message, sender); + + String nodeAddress = (message!=null) ? message.toString() : null; + log.trace("Collectors::{}: nodeAddress={}", collectorId, nodeAddress); + + if (RecoveryConstant.SELF_HEALING_RECOVERY_COMPLETED.equals(topic)) { + log.info("Collectors::{}: Resuming collection from Node: {}", collectorId, nodeAddress); + ignoredNodes.remove(nodeAddress); + } else + if (RecoveryConstant.SELF_HEALING_RECOVERY_GIVE_UP.equals(topic)) { + log.warn("Collectors::{}: Giving up collection from Node: {}", collectorId, nodeAddress); + ignoredNodes.put(nodeAddress, null); + } else + if (EventConstant.EVENT_CLIENT_CONFIG_UPDATED.equals(topic)) { + log.info("Collectors::{}: Client configuration updated. Purging nodes without recovery task from ignore list: Old ignore list nodes: {}", collectorId, ignoredNodes.keySet()); + List nodesToPurge = ignoredNodes.entrySet().stream().filter(e -> e.getValue() == null).map(Map.Entry::getKey).collect(Collectors.toList()); + nodesToPurge.forEach(node -> { + ignoredNodes.remove(node); + log.info("Collectors::{}: Client configuration updated. Node purged from ignore list: {}", collectorId, node); + }); + } else + log.warn("Collectors::{}: onMessage: Event from unexpected topic received. Ignoring it: {}", collectorId, topic); + } + + public void run() { + if (!started) return; + + log.trace("Collectors::{}: run(): BEGIN", collectorId); + if (log.isTraceEnabled()) { + log.trace("Collectors::{}: run(): errors-map={}", collectorId, errorsMap); + log.trace("Collectors::{}: run(): ignored-nodes={}", collectorId, ignoredNodes.keySet()); + } + + // collect data from local node + if (! properties.isSkipLocal()) { + log.debug/*info*/("Collectors::{}: Collecting metrics from local node...", collectorId); + collectAndPublishData(""); + } else { + log.debug("Collectors::{}: Collection from local node is disabled", collectorId); + } + + // if Aggregator, collect data from nodes without client + log.trace("Collectors::{}: Nodes without clients in Zone: {}", collectorId, collectorContext.getNodesWithoutClient()); + log.trace("Collectors::{}: Is Aggregator: {}", collectorId, collectorContext.isAggregator()); + if (collectorContext.isAggregator()) { + if (collectorContext.getNodesWithoutClient().size()>0) { + log.debug/*info*/("Collectors::{}: Collecting metrics from remote nodes (without EMS client): {}", collectorId, + collectorContext.getNodesWithoutClient()); + for (Object nodeAddress : collectorContext.getNodesWithoutClient()) { + // collect data from remote node + collectAndPublishData(nodeAddress.toString()); + } + } else + log.debug("Collectors::{}: No remote nodes (without EMS client)", collectorId); + } + + log.trace("Collectors::{}: run(): END", collectorId); + } + + protected void registerInternalEvents(@NonNull String prefix) { + registerInternalEvents( + prefix + BASE_COLLECTION_START, + prefix + BASE_COLLECTION_END, + prefix + BASE_COLLECTION_ERROR, + prefix + BASE_CONN_OK, + prefix + BASE_CONN_ERROR, + prefix + BASE_NODE_OK, + prefix + BASE_NODE_FAILED); + } + + @SuppressWarnings("unchecked") + protected Class> getCollectorClass() { + return (Class>) getClass(); + } + + protected void registerInternalEvents(@NonNull String collectionStartEvent, + @NonNull String collectionEndEvent, + @NonNull String collectionErrorEvent, + @NonNull String connectionOkEvent, + @NonNull String connectionErrorEvent, + @NonNull String nodeOkEvent, + @NonNull String nodeFailedEvent) { + Map collectorEvents = new LinkedHashMap<>(); + collectorEvents.put(EVENT_COLLECTION_START, collectionStartEvent); + collectorEvents.put(EVENT_COLLECTION_END, collectionEndEvent); + collectorEvents.put(EVENT_COLLECTION_ERROR, collectionErrorEvent); + collectorEvents.put(EVENT_CONN_OK, connectionOkEvent); + collectorEvents.put(EVENT_CONN_ERROR, connectionErrorEvent); + collectorEvents.put(EVENT_NODE_OK, nodeOkEvent); + collectorEvents.put(EVENT_NODE_FAILED, nodeFailedEvent); + log.debug("Collectors::{}: registerInternalEvents: BEFORE REGISTRATION: collector-class={}, events={}", collectorId, getClass(), collectorEvents); + + Class> clazz = getCollectorClass(); + nodeToNodeEventsMap.put(clazz, collectorEvents); + log.debug("Collectors::{}: registerInternalEvents: AFTER REGISTRATION: collector-class={}, events={}", collectorId, clazz, collectorEvents); + } + + private Map getInternalEvents() { + log.debug("Collectors::{}: getInternalEvents: BEGIN: collector-class={}", collectorId, getClass()); + Class> clazz = getCollectorClass(); + Map collectorEvents = nodeToNodeEventsMap.get(clazz); + log.debug("Collectors::{}: getInternalEvents: END: collector-class={}, events={}", collectorId, clazz, collectorEvents); + return collectorEvents; + } + + private COLLECTION_RESULT collectAndPublishData(@NonNull String nodeAddress) { + if (ignoredNodes.containsKey(nodeAddress)) { + log.debug/*info*/("Collectors::{}: Node is in ignore list: {}", collectorId, nodeAddress); + return COLLECTION_RESULT.IGNORED; + } + + Map nodeEvents = getInternalEvents(); + try { + sendEvent(nodeEvents.get(EVENT_COLLECTION_START), nodeAddress); + _collectAndPublishData(nodeAddress); + sendEvent(nodeEvents.get(EVENT_COLLECTION_END), nodeAddress); + + //if (Optional.ofNullable(errorsMap.put(nodeAddress, 0)).orElse(0)>0) sendEvent(ABSTRACT_ENDPOINT_CONN_OK, nodeAddress); + sendEvent(nodeEvents.get(EVENT_CONN_OK), nodeAddress); + sendEvent(nodeEvents.get(EVENT_NODE_OK), nodeAddress); + errorsMap.put(nodeAddress, 0); + return COLLECTION_RESULT.OK; + } catch (Throwable t) { + int errors = errorsMap.compute(nodeAddress, (k, v) -> Optional.ofNullable(v).orElse(0) + 1); + int errorLimit = properties.getErrorLimit(); + log.warn("Collectors::{}: Exception while collecting metrics from node: {}, #errors={}, exception: {}", + collectorId, nodeAddress, errors, getExceptionMessages(t)); + log.debug("Collectors::{}: Exception while collecting metrics from node: {}, #errors={}\n", collectorId, nodeAddress, errors, t); + + sendEvent(nodeEvents.get(EVENT_COLLECTION_ERROR), nodeAddress, "errors="+errors); + sendEvent(nodeEvents.get(EVENT_CONN_ERROR), nodeAddress, "errors="+errors); + + if (errorLimit<=0 || errors >= errorLimit) { + log.warn("Collectors::{}: Too many consecutive errors occurred while attempting to collect metrics from node: {}, num-of-errors={}", collectorId, nodeAddress, errors); + log.warn("Collectors::{}: Pausing collection from Node: {}", collectorId, nodeAddress); + ignoredNodes.put(nodeAddress, null); + sendEvent(nodeEvents.get(EVENT_NODE_FAILED), nodeAddress); + } + return COLLECTION_RESULT.ERROR; + } + } + + private String getExceptionMessages(Throwable t) { + StringBuilder sb = new StringBuilder(); + while (t!=null) { + sb.append(" -> ").append(t.getClass().getName()).append(": ").append(t.getMessage()); + t = t.getCause(); + } + return sb.substring(4); + } + + private void sendEvent(String topic, String nodeAddress, String...extra) { + Map message = new HashMap<>(); + message.put("address", nodeAddress); + for (String e : extra) { + String[] s = e.split("[:=]", 2); + if (s.length==2 && StringUtils.isNotBlank(s[0])) + message.put(s[0].trim(), s[1]); + } + eventBus.send(topic, message, getClass().getName()); + } + + protected abstract ResponseEntity getData(String url); + protected abstract void processData(T data, String nodeAddress, ProcessingStats stats); + + private void _collectAndPublishData(String nodeAddress) { + String url; + if (StringUtils.isBlank(nodeAddress)) { + // Local node data collection URL + url = properties.getUrl(); + if (StringUtils.isBlank(url)) + url = String.format(properties.getUrlOfNodesWithoutClient(), "127.0.0.1"); + } else { + // Remote node data collection URL + url = String.format(properties.getUrlOfNodesWithoutClient(), nodeAddress); + } + log.debug/*info*/("Collectors::{}: Collecting data from url: {}", collectorId, url); + + log.debug("Collectors::{}: Collecting data: {}...", collectorId, url); + long startTm = System.currentTimeMillis(); + ResponseEntity response = getData(url); + long callEndTm = System.currentTimeMillis(); + log.trace("Collectors::{}: ...response: {}", collectorId, response); + + if (response.getStatusCode()==HttpStatus.OK) { + T data = response.getBody(); + ProcessingStats stats = new ProcessingStats(); + + log.trace("Collectors::{}: Processing data started: data: {}", collectorId, data); + processData(data, nodeAddress, stats); + log.trace("Collectors::{}: Processing data completed: stats: {}", collectorId, stats); + + long endTm = System.currentTimeMillis(); + log.debug("Collectors::{}: Collecting data...ok", collectorId); + //log.info("Collectors::{}: Metrics: extracted={}, published={}, failed={}", collectorId, + // stats.countSuccess + stats.countErrors, stats.countSuccess, stats.countErrors); + if (log.isInfoEnabled()) + log.debug/*info*/("Collectors::{}: Publish statistics: {}", collectorId, stats); + log.debug("Collectors::{}: Durations: rest-call={}, extract+publish={}, total={}", collectorId, + callEndTm-startTm, endTm-callEndTm, endTm-startTm); + } else { + log.warn("Collectors::{}: Collecting data...failed: Http Status: {}", collectorId, response.getStatusCode()); + } + } + + protected CollectorContext.PUBLISH_RESULT publishMetricEvent(String metricName, double metricValue, long timestamp, String nodeAddress) { + EventMap event = new EventMap(metricValue, 1, timestamp); + return publishMetricEvent(metricName, event, nodeAddress); + } + + protected CollectorContext.PUBLISH_RESULT publishMetricEvent(String metricName, EventMap event, String nodeAddress) { + boolean createTopic = properties.isCreateTopic(); + try { + String originalTopic = metricName; + boolean createDestination = (createTopic || allowedTopics!=null && allowedTopics.contains(metricName)); + if (topicMap!=null) { + String targetTopic = topicMap.get(metricName); + if (targetTopic!=null && !targetTopic.isEmpty()) + metricName = targetTopic; + } + event.setEventProperty(EmsConstant.EVENT_PROPERTY_SOURCE_ADDRESS, nodeAddress); + event.getEventProperties().put(EmsConstant.EVENT_PROPERTY_EFFECTIVE_DESTINATION, metricName); + event.getEventProperties().put(EmsConstant.EVENT_PROPERTY_ORIGINAL_DESTINATION, originalTopic); + log.debug("Collectors::{}: Publishing metric: {}: {}", collectorId, metricName, event.getMetricValue()); + CollectorContext.PUBLISH_RESULT result = collectorContext.sendEvent(null, metricName, event, createDestination); + log.trace("Collectors::{}: Publishing metric: {}: {} -> result: {}", collectorId, metricName, event.getMetricValue(), result); + return result; + } catch (Exception e) { + log.warn("Collectors::{}: Publishing metric failed: ", collectorId, e); + return CollectorContext.PUBLISH_RESULT.ERROR; + } + } + + protected void updateStats(CollectorContext.PUBLISH_RESULT publishResult, ProcessingStats stats) { + if (publishResult==CollectorContext.PUBLISH_RESULT.SENT) stats.countSuccess++; + else if (publishResult==CollectorContext.PUBLISH_RESULT.SKIPPED) stats.countSkipped++; + else if (publishResult==CollectorContext.PUBLISH_RESULT.ERROR) stats.countErrors++; + } + + protected static class ProcessingStats { + public int countSuccess; + public int countErrors; + public int countSkipped; + + public int getCountTotal() { + return countSuccess+countSkipped+countErrors; + } + + public String toString() { + return String.format("extracted: %d, published: %d, skipped: %d, failed: %d", getCountTotal(), countSuccess, countSkipped, countErrors); + } + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/AbstractEndpointCollectorProperties.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/AbstractEndpointCollectorProperties.java new file mode 100644 index 0000000..68adc5d --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/AbstractEndpointCollectorProperties.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; + +import java.util.List; + +@Slf4j +@Data +public class AbstractEndpointCollectorProperties implements InitializingBean { + private boolean enable; + private long delay; + private String url; + private String urlOfNodesWithoutClient; + private boolean skipLocal = false; + private boolean createTopic; + private List allowedTopics; + + private int errorLimit; // num of consecutive errors. Zero or negative value will immediately trigger self-healing + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("AbstractEndpointCollectorProperties: {}", this); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/CollectorContext.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/CollectorContext.java new file mode 100644 index 0000000..a5eae64 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/CollectorContext.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector; + +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.common.client.SshClient; +import gr.iccs.imu.ems.common.client.SshClientProperties; +import gr.iccs.imu.ems.util.ClientConfiguration; + +import java.io.Serializable; +import java.util.List; +import java.util.Set; + +public interface CollectorContext

{ + enum PUBLISH_RESULT { SENT, SKIPPED, ERROR } + + List getNodeConfigurations(); + Set getNodesWithoutClient(); + boolean isAggregator(); + PUBLISH_RESULT sendEvent(String connectionString, String destinationName, EventMap event, boolean createDestination); + default SshClient

getSshClient() { return null; } + default P getSshClientProperties() { return null; } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/netdata/NetdataCollector.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/netdata/NetdataCollector.java new file mode 100644 index 0000000..ef4fd9b --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/netdata/NetdataCollector.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector.netdata; + +import gr.iccs.imu.ems.common.collector.AbstractEndpointCollector; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.util.EventBus; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +/** + * Collects measurements from Netdata http server + */ +@Slf4j +public class NetdataCollector extends AbstractEndpointCollector { + public final static String NETDATA_COLLECTION_START = "NETDATA_COLLECTION_START"; + public final static String NETDATA_COLLECTION_OK = "NETDATA_COLLECTION_OK"; + public final static String NETDATA_COLLECTION_ERROR = "NETDATA_COLLECTION_ERROR"; + public final static String NETDATA_CONN_OK = "NETDATA_CONN_OK"; + public final static String NETDATA_CONN_ERROR = "NETDATA_CONN_ERROR"; + public final static String NETDATA_NODE_OK = "NETDATA_NODE_OK"; + public final static String NETDATA_NODE_FAILED = "NETDATA_NODE_FAILED"; + + protected NetdataCollectorProperties properties; + protected RestTemplate restTemplate = new RestTemplate(); + + @SuppressWarnings("unchecked") + public NetdataCollector(String id, NetdataCollectorProperties properties, CollectorContext collectorContext, TaskScheduler taskScheduler, EventBus eventBus) { + super(id, properties, collectorContext, taskScheduler, eventBus); + this.properties = properties; + } + + @Override + public void afterPropertiesSet() { + log.debug("Collectors::Netdata: properties: {}", properties); + super.afterPropertiesSet(); + + if (StringUtils.isBlank(properties.getUrl())) { + String url = "http://127.0.0.1:19999/api/v1/allmetrics?format=json"; + log.debug("Collectors::Netdata: URL not specified. Assuming {}", url); + properties.setUrl(url); + } + + this.restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(5)) + .build(); + + registerInternalEvents(NETDATA_COLLECTION_START, NETDATA_COLLECTION_OK, NETDATA_COLLECTION_ERROR, + NETDATA_CONN_OK, NETDATA_CONN_ERROR, NETDATA_NODE_OK, NETDATA_NODE_FAILED); + } + + protected ResponseEntity getData(String url) { + return restTemplate.getForEntity(url, HashMap.class); + } + + protected void processData(HashMap data, String nodeAddress, ProcessingStats stats) { + Map dataMap = data; + for (Object key : dataMap.keySet()) { + log.trace("Collectors::Netdata: ...Loop-1: key={}", key); + if (key==null) continue; + Map keyData = (Map)dataMap.get(key); + log.trace("Collectors::Netdata: ...Loop-1: key-data={}", keyData); + long timestamp = Long.parseLong( keyData.get("last_updated").toString() ); + Map dimensionsMap = (Map)keyData.get("dimensions"); + + log.trace("Collectors::Netdata: ...Loop-1: ...dimensions-keys: {}", dimensionsMap.keySet()); + for (Object dimKey : dimensionsMap.keySet()) { + log.trace("Collectors::Netdata: ...Loop-1: ...dimensions-key: {}", dimKey); + if (dimKey==null) continue; + String metricName = ("netdata."+ key + "."+ dimKey).replace(".", "__"); + log.trace("Collectors::Netdata: ...Loop-1: ...metric-name: {}", metricName); + Map dimData = (Map)dimensionsMap.get(dimKey); + Object valObj = dimData.get("value"); + log.trace("Collectors::Netdata: ...Loop-1: ...metric-value: {}", valObj); + if (valObj!=null) { + double metricValue = Double.parseDouble(valObj.toString()); + log.trace("Collectors::Netdata: {} = {}", metricName, metricValue); + + updateStats(publishMetricEvent(metricName, metricValue, timestamp, nodeAddress), stats); + } + } + + if (Thread.currentThread().isInterrupted()) break; + } + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/netdata/NetdataCollectorProperties.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/netdata/NetdataCollectorProperties.java new file mode 100644 index 0000000..c8ef7e2 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/netdata/NetdataCollectorProperties.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector.netdata; + +import gr.iccs.imu.ems.common.collector.AbstractEndpointCollectorProperties; +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Data +@EqualsAndHashCode(callSuper = true) +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "collector.netdata") +public class NetdataCollectorProperties extends AbstractEndpointCollectorProperties { + @Override + public void afterPropertiesSet() throws Exception { + log.debug("NetdataCollectorProperties: {}", this); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/OpenMetricsParser.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/OpenMetricsParser.java new file mode 100644 index 0000000..1487570 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/OpenMetricsParser.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector.prometheus; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +/** + * Parses OpenMetrics-formatted input + */ +@Slf4j +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OpenMetricsParser { + + public static void main(String[] args) throws IOException { + OpenMetricsParser parser = new OpenMetricsParser(); + for (String file : args) { + log.info("Processing file: {}", file); + List lines = Files.readAllLines(Paths.get(file)); + List metricInstances = parser.processInput(lines.toArray(new String[0])); + log.info("Results:\n{}", metricInstances); + } + } + + private boolean throwExceptionWhenExcessiveCharsOccur; + + public List processInput(String[] lines) { + LinkedHashMap tags = new LinkedHashMap<>(); + + ProcessingContext context = new ProcessingContext(); + List results = new ArrayList<>(); + for (String line : lines) { + log.debug("OpenMetricsParser: processInput: Looping...: line: {}", line); + line = line.trim(); + if (line.isEmpty()) { + log.trace("OpenMetricsParser: Skip blank line"); + continue; + } + + MetricInstance metricInstance = processLine(line, tags, context); + if (metricInstance!=null) + results.add( metricInstance ); + } + return results; + } + + public MetricInstance processLine(@NonNull String line) { + return processLine(line, new LinkedHashMap<>(), null); + } + + public MetricInstance processLine(@NonNull String line, @NonNull Map tags, ProcessingContext context) { + try { + if (line.charAt(0) == '#') { + line = line.substring(1).trim(); + String[] part = line.split(" ", 2); + + if ("HELP".equalsIgnoreCase(part[0])) { + log.debug("OpenMetricsParser: processLine: Found HELP line: {}", line); // Ignore HELP line + if (part.length<2) + throw new MalformedMetricLineException("HELP line is malformed: "+line); + part = part[1].split("[ \t\r]+", 2); + if (StringUtils.isBlank(part[0])) + throw new MalformedMetricLineException("HELP line is malformed: "+line); + String newMetricName = part[0].trim(); + String helpText = part.length>1 ? part[1] : null; + if (context.getMetricHelpTexts().containsKey(newMetricName)) + throw new MalformedMetricLineException("HELP for metric has already been set: " + newMetricName); + context.getMetricHelpTexts().put(newMetricName, processHelpText(helpText)); + + } else if ("TYPE".equalsIgnoreCase(part[0])) { + log.debug("OpenMetricsParser: processLine: Found TYPE line: {}", line); // Ignore TYPE line + if (part.length<2) + throw new MalformedMetricLineException("TYPE line is malformed: "+line); + part = part[1].split("[ \t\r]+"); + if (part.length!=2) + throw new MalformedMetricLineException("TYPE line is malformed: "+line); + if (StringUtils.isBlank(part[0])) + throw new MalformedMetricLineException("TYPE line is malformed: "+line); + String newMetricName = part[0].trim(); + METRIC_TYPE newMetricType = METRIC_TYPE.valueOf(part[1].trim().toUpperCase()); + if (context.getMetricTypes().containsKey(newMetricName)) + throw new MalformedMetricLineException("TYPE for metric has already been set: " + newMetricName); + context.getMetricTypes().put(newMetricName, newMetricType); + } else + log.debug("OpenMetricsParser: processLine: Found comment line: {}", line); // Ignore comment + + return null; + + } else { + log.debug("OpenMetricsParser: processLine: Found metric line: {}", line); + + // init line processing + int i = 0; + int lineLength = line.length(); + tags.clear(); + + // get metric name + String metricName = getIdentifier(line, i); + log.trace("OpenMetricsParser: processLine: metricName: {}", metricName); + i += metricName.length(); + i = skipWhites(line, i); + + // check for tag list opening ('{') + if (line.charAt(i)=='{') { + // tag list found... skip white chars + i = skipWhites(line, i+1); + + // process tags... + while (true) { + // get tag name + String tagName = getIdentifier(line, i); + log.trace("OpenMetricsParser: processLine: tagName: {}", tagName); + i += tagName.length(); + i = skipWhites(line, i); + + if (line.charAt(i)!='=') + throw new MalformedMetricLineException("Expected '=' after tag name"); + i = skipWhites(line, i+1); + + // get tag value + String tagValue = getTagValue(line, i); + i += tagValue.length(); + i++; // skip tag value closing quote + i = skipWhites(line, i+1); + tagValue = processEscapeSequences(tagValue); + log.trace("OpenMetricsParser: processLine: tagValue: {}", tagValue); + + if (i==lineLength) + throw new MalformedMetricLineException("Line end reached. Tag list not closed after last tag value"); + + // check for a comma following tag value + boolean commaFound = false; + if (line.charAt(i)==',') { + commaFound = true; + i = skipWhites(line, i+1); + if (i==lineLength) + throw new MalformedMetricLineException("Line end reached. Tag list not closed after last comma"); + } + + // add tag pair in tags map + log.trace("OpenMetricsParser: processLine: tag pair: {} = {}", tagName, tagValue); + tags.put(tagName, tagValue); + + // check for tag list closing + if (line.charAt(i)=='}') { + i = skipWhites(line, i+1); + break; + } else if (!commaFound) + throw new MalformedMetricLineException("Expected ',' or '}' after tag value"); + else + ; // repeat + } + } + if (i==lineLength) + throw new MalformedMetricLineException("Line end reached. No metric value found after tag list"); + + // get metric value + String valueStr = getNonWhites(line, i); + log.trace("OpenMetricsParser: metricValue: {}", valueStr); + if (valueStr.isEmpty()) + throw new MalformedMetricLineException("No valid metric value found"); + i += valueStr.length(); + + // check for (optional) timestamp + String tmStr = null; + if (istart)) i++; + String identifier = line.substring(start, i); + if (identifier.isEmpty()) throw new MalformedMetricLineException("No valid identifier found"); + return identifier; + } + + protected String getTagValue(String line, int i) { + int start = i; + int lineLength = line.length(); + + // check for tag value opening quote (") + if (line.charAt(i)!='\"') + throw new MalformedMetricLineException("Expected '\"' to open tag value"); + i++; + + // read tag value (chars until first unescaped quote) + while (i tags, ProcessingContext context) { + // Prepare value + valueStr = valueStr.trim(); + double value; + try { + if ("NaN".equalsIgnoreCase(valueStr)) value = Double.NaN; + else if ("Inf".equalsIgnoreCase(valueStr)) value = Double.POSITIVE_INFINITY; + else if ("+Inf".equalsIgnoreCase(valueStr)) value = Double.POSITIVE_INFINITY; + else if ("-Inf".equalsIgnoreCase(valueStr)) value = Double.NEGATIVE_INFINITY; + else value = Double.parseDouble(valueStr); + } catch (Exception e) { + throw new MalformedMetricLineException("Invalid metric value: "+valueStr, e); + } + + // Prepare timestamp + long timestamp; + try { + timestamp = (tmStr != null && !tmStr.trim().isEmpty()) ? Long.parseLong(tmStr.trim()) : System.currentTimeMillis(); + } catch (Exception e) { + throw new MalformedMetricLineException("Invalid timestamp: "+tmStr, e); + } + + // Prepare type and help text + METRIC_TYPE metricType = context!=null ? context.getMetricTypes().computeIfAbsent(metricName, s->METRIC_TYPE.UNTYPED) : METRIC_TYPE.UNTYPED; + String helpText = context!=null ? context.getMetricHelpTexts().computeIfAbsent(metricName, s->null) : null; + + // Create metric instance + return MetricInstance.builder() + .metricName(metricName) + .metricType(metricType) + .metricValue(value) + .timestamp(timestamp) + .tags(new LinkedHashMap<>(tags)) + .helpText(helpText) + .build(); + } + + public enum METRIC_TYPE { UNTYPED, COUNTER, GAUGE, HISTOGRAM, SUMMARY } + + @Data + public static class ProcessingContext { + private Map metricTypes = new HashMap<>(); + private Map metricHelpTexts = new HashMap<>(); + } + + @Data + @Builder + public static class MetricInstance { + @NonNull private final String metricName; + @NonNull private final METRIC_TYPE metricType; + private final double metricValue; + private final long timestamp; + private final Map tags; + private final String helpText; + } + + public static class MalformedMetricLineException extends RuntimeException { + public MalformedMetricLineException() { super(); } + public MalformedMetricLineException(String message) { super(message); } + public MalformedMetricLineException(String message, Throwable t) { super(message, t); } + public MalformedMetricLineException(Throwable t) { super(t); } + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/PrometheusCollector.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/PrometheusCollector.java new file mode 100644 index 0000000..751a04d --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/PrometheusCollector.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector.prometheus; + +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.common.collector.AbstractEndpointCollector; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.util.EventBus; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Collects measurements from a Prometheus exporter endpoint + */ +@Slf4j +public class PrometheusCollector extends AbstractEndpointCollector { + protected PrometheusCollectorProperties properties; + protected RestTemplate restTemplate = new RestTemplate(); + + private Set allowedTags; + private boolean allowTagsInDestinationName; + private String destinationNameFormatter = "${metricName}"; + private boolean addTagsAsEventProperties; + private boolean addTagsInEventPayload; + + @SuppressWarnings("unchecked") + public PrometheusCollector(String id, PrometheusCollectorProperties properties, CollectorContext collectorContext, TaskScheduler taskScheduler, EventBus eventBus) { + super(id, properties, collectorContext, taskScheduler, eventBus); + this.properties = properties; + } + + @Override + public void afterPropertiesSet() { + log.debug("Collectors::{}: properties: {}", collectorId, properties); + super.afterPropertiesSet(); + + this.allowedTags = properties.getAllowedTags(); + this.allowTagsInDestinationName = properties.isAllowTagsInDestinationName(); + this.destinationNameFormatter = properties.getDestinationNameFormatter(); + this.addTagsAsEventProperties = properties.isAddTagsAsEventProperties(); + this.addTagsInEventPayload = properties.isAddTagsInEventPayload(); + + if (StringUtils.isBlank(properties.getUrl())) { + String url = "http://127.0.0.1:9090/metrics"; + log.debug("Collectors::{}: URL not specified. Assuming {}", collectorId, url); + properties.setUrl(url); + } + + this.restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(5)) + .build(); + } + + protected ResponseEntity getData(String url) { + return restTemplate.getForEntity(url, String.class); + } + + protected void processData(String data, String nodeAddress, ProcessingStats stats) { + String[] lines = data.split("\n"); + + List metricInstances = + new OpenMetricsParser(properties.isThrowExceptionWhenExcessiveCharsOccur()).processInput(lines); + log.debug("Collectors::{}: Metric instances extracted: {}", collectorId, metricInstances); + + for (OpenMetricsParser.MetricInstance instance : metricInstances) { + // Create event + EventMap event = new EventMap(instance.getMetricValue(), 1, instance.getTimestamp()); + + // Add tags into event properties and/or payload + Map tags = instance.getTags(); + if (tags != null) { + if (allowedTags != null && allowedTags.size() > 0) { + tags.keySet().retainAll(allowedTags); + } + + if (addTagsAsEventProperties) + event.getEventProperties().putAll(tags); + if (addTagsInEventPayload) + event.putAll(tags); + } + + // Get destination names and publish event + String baseMetricName = instance.getMetricName(); + String destination = StringUtils.isNotBlank(destinationNameFormatter) + ? destinationNameFormatter.replace("${metricName}", baseMetricName) + : baseMetricName; + log.debug("Collectors::{}: Metric instances extracted: {}", collectorId, destination); + + if (!destination.contains("${")) { + log.debug("Collectors::{}: Publishing event to destination: {}", collectorId, destination); + updateStats(publishMetricEvent(destination, event, nodeAddress), stats); + } else + if (allowTagsInDestinationName && tags!=null && tags.size()>0) { + tags.forEach((name,value) -> { + String d = destination.replace("${"+name+"}", value); + log.debug("Collectors::{}: Publishing event to tagged destination: {}", collectorId, d); + updateStats(publishMetricEvent(d, event, nodeAddress), stats); + }); + } + + if (Thread.currentThread().isInterrupted()) break; + } + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/PrometheusCollectorProperties.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/PrometheusCollectorProperties.java new file mode 100644 index 0000000..8b226de --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/collector/prometheus/PrometheusCollectorProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.collector.prometheus; + +import gr.iccs.imu.ems.common.collector.AbstractEndpointCollectorProperties; +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Set; + +@Slf4j +@Data +@EqualsAndHashCode(callSuper = true) +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "collector.prometheus") +public class PrometheusCollectorProperties extends AbstractEndpointCollectorProperties { + private Set allowedTags; + private boolean allowTagsInDestinationName; + private String destinationNameFormatter = "${metricName}"; + private boolean addTagsAsEventProperties; + private boolean addTagsInEventPayload; + private boolean throwExceptionWhenExcessiveCharsOccur; + + public PrometheusCollectorProperties() { + setUrl("http://127.0.0.1:9090/metrics"); + setUrlOfNodesWithoutClient("http://%s:9090/metrics"); + } + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("PrometheusCollectorProperties: {}", this); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/misc/EventConstant.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/misc/EventConstant.java new file mode 100644 index 0000000..da7ec7c --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/misc/EventConstant.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.misc; + +/** + * Common Event Constants + */ +public class EventConstant { + public final static String EVENT_CLIENT_CONFIG_UPDATED = "EVENT_CLIENT_CONFIG_UPDATED"; +} \ No newline at end of file diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/misc/SystemResourceMonitor.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/misc/SystemResourceMonitor.java new file mode 100644 index 0000000..435f89f --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/misc/SystemResourceMonitor.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.misc; + +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SystemResourceMonitor implements Runnable, InitializingBean { + @Getter @Setter + private boolean enabled = Boolean.parseBoolean( + System.getenv().getOrDefault("EMS_SYSMON_ENABLED", "true")); + @Getter @Setter + private long period = Math.max(1000L,Long.parseLong( + System.getenv().getOrDefault("EMS_SYSMON_PERIOD", "30000"))); + @Getter @Setter + private String commandStr = System.getenv().getOrDefault("EMS_SYSMON_COMMAND", "./bin/sysmon.sh"); + @Getter @Setter + private String systemResourceMetricsTopic = System.getenv("EMS_SYSMON_TOPIC"); + @Getter @Setter + private boolean publishAsMetrics = Boolean.parseBoolean( + Objects.requireNonNullElse(System.getenv("EMS_SYSMON_PUBLISH_AS_METRICS"), "false") ); + + private final BrokerCepService brokerCepService; + private final TaskScheduler scheduler; + private ScheduledFuture future; + @Getter + private Map latestMeasurements; + + private final Map topicsCache = new HashMap<>(); + + @Override + public void afterPropertiesSet() throws Exception { + if (!enabled) log.warn("SystemResourceMonitor is disabled"); + else start(); + } + + public void start() { + if (!enabled) return; + if (future!=null) { + log.warn("SystemResourceMonitor is already running"); + return; + } + future = scheduler.scheduleAtFixedRate(this, Duration.ofMillis(period)); + log.info("SystemResourceMonitor started"); + } + + public void stop() { + if (!enabled) return; + if (future==null || future.isCancelled()) { + log.warn("SystemResourceMonitor is already stopped"); + return; + } + future.cancel(true); + future = null; + topicsCache.clear(); + log.info("SystemResourceMonitor stopped"); + } + + public void run() { + if (!enabled) return; + StringBuilder result = new StringBuilder(); + try { + if (StringUtils.isBlank(commandStr)) { + log.debug("SystemResourceMonitor: Nothing to do. System metrics command is blank: {}", commandStr); + return; + } + log.debug("SystemResourceMonitor: Getting system metrics with command: {}", commandStr); + Runtime r = Runtime.getRuntime(); + Process p = r.exec(commandStr); + BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + result.append(inputLine).append("\n"); + } + in.close(); + log.debug("SystemResourceMonitor: Script output:\n{}", result); + + if (publishAsMetrics) + processOutputAsMetrics(result.toString()); + else + processOutput(result.toString()); + + } catch (IOException e) { + log.warn("SystemResourceMonitor: EXCEPTION: ", e); + } + } + + @SneakyThrows + private void processOutput(String result) { + log.debug("SystemResourceMonitor: processOutput: BEGIN:\n{}", result); + if (StringUtils.isBlank(systemResourceMetricsTopic)) { + log.debug("SystemResourceMonitor: processOutput: END: No metrics topic has been set. Will not publish metrics event"); + return; + } + + EventMap event = new EventMap(); + for (String line : result.split("\n")) { + String[] part = line.split(":", 2); + String metricName = part[0].trim().toLowerCase(); + double metricValue= Double.parseDouble(part[1].trim()); + event.put(metricName, metricValue); + } + this.latestMeasurements = Collections.unmodifiableMap(event); + log.debug("SystemResourceMonitor: processOutput: Metrics: {}", event); + + log.trace("SystemResourceMonitor: processOutput: Will publish metrics event to topic: {}", systemResourceMetricsTopic); + brokerCepService.publishEvent(null, systemResourceMetricsTopic, event); + log.debug("SystemResourceMonitor: processOutput: END: Metrics event published to topic: {}", systemResourceMetricsTopic); + } + + @SneakyThrows + private void processOutputAsMetrics(String result) { + log.debug("SystemResourceMonitor: processOutputNew: BEGIN:\n{}", result); + if (StringUtils.isBlank(systemResourceMetricsTopic)) { + log.debug("SystemResourceMonitor: processOutputNew: END: No metrics topic has been set. Will not publish metrics event"); + return; + } + + EventMap latest = new EventMap(); + for (String line : result.split("\n")) { + String[] part = line.split(":", 2); + String metricName = part[0].trim().toLowerCase(); + double metricValue= Double.parseDouble(part[1].trim()); + latest.put(metricName, metricValue); + + String topic = topicsCache.computeIfAbsent(metricName, s -> systemResourceMetricsTopic + s.trim().toUpperCase()); + log.trace("SystemResourceMonitor: processOutputNew: Will publish {} metric event to topic: {}", metricName, topic); + brokerCepService.publishEvent(null, topic, new EventMap(metricValue)); + log.trace("SystemResourceMonitor: processOutputNew: END: {} metric event published to topic: {}", metricName, topic); + } + this.latestMeasurements = Collections.unmodifiableMap(latest); + log.debug("SystemResourceMonitor: processOutputNew: END: Latest Metrics: {}", latest); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/plugin/PluginManager.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/plugin/PluginManager.java new file mode 100644 index 0000000..9957936 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/plugin/PluginManager.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.plugin; + +import gr.iccs.imu.ems.util.Plugin; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Plugin Manager + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PluginManager implements InitializingBean { + private List activePlugins = new LinkedList<>(); + + @Override + public void afterPropertiesSet() throws Exception { + log.info("PluginManager: Started"); + } + + @SafeVarargs + public final void initializePlugins(Class... pluginClasses) { + initializePlugins(Arrays.asList(pluginClasses)); + } + + public void initializePlugins(@NonNull List> pluginClasses) { + pluginClasses.forEach(this::initializePlugin); + } + + @SneakyThrows + public synchronized void initializePlugin(Class pluginClass) { + Plugin plugin = pluginClass.getConstructor().newInstance(); + activePlugins.add(plugin); + plugin.start(); + } + + public synchronized void stopPlugins() { + activePlugins.forEach(Plugin::stop); + activePlugins.clear(); + } + + public synchronized void stopPlugin(@NonNull Plugin plugin) { + if (activePlugins.contains(plugin)) { + activePlugins.remove(plugin); + plugin.stop(); + } + } + + public List getActivePlugins() { + return Collections.unmodifiableList(activePlugins); + } + + public List getActivePlugins(@NonNull Class type) { + return activePlugins.stream() + .filter(plugin -> type.isAssignableFrom(plugin.getClass())) + .collect(Collectors.toList()); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/AbstractRecoveryTask.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/AbstractRecoveryTask.java new file mode 100644 index 0000000..cddddd1 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/AbstractRecoveryTask.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.text.StringSubstitutor; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +@Component +@RequiredArgsConstructor +public abstract class AbstractRecoveryTask implements RecoveryTask { + @NonNull protected final EventBus eventBus; + @NonNull protected final PasswordUtil passwordUtil; + @NonNull protected final TaskScheduler taskScheduler; + @NonNull protected final SelfHealingProperties selfHealingProperties; + + @NonNull + @Getter @Setter + protected Map nodeInfo = Collections.emptyMap(); + + public abstract List getRecoveryCommands(); + public abstract void runNodeRecovery(RecoveryContext recoveryContext) throws Exception; + public abstract void runNodeRecovery(List recoveryCommands, RecoveryContext recoveryContext) throws Exception; + + protected void waitFor(long millis, String description) { + if (millis>0) { + log.warn("############## Waiting for {}ms after {}...", millis, description); + try { Thread.sleep(millis); } catch (InterruptedException ignored) { } + } + } + + protected void redirectOutput(InputStream in, String id, AtomicBoolean closed, String connectionClosedMessageFormatter, String exceptionMessageFormatter) { + taskScheduler.schedule(() -> { + try { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + while (reader.ready()) { + log.info(" {}> {}", id, reader.readLine()); + } + } + } catch (IOException e) { + if (closed.get()) { + log.info(connectionClosedMessageFormatter, id); + } else { + log.error(exceptionMessageFormatter, id, e); + } + } + }, + Instant.now() + ); + } + + protected String prepareCommandString(String command, RecoveryContext recoveryContext) { + log.trace("AbstractRecoveryTask.prepareCommandString: BEGIN: {}", command); + command = StringSubstitutor.replaceSystemProperties(command); + log.trace("AbstractRecoveryTask.prepareCommandString: AFTER replaceSystemProperties: {}", command); + Map variablesMap = recoveryContext.getVariablesMap(); + log.trace("AbstractRecoveryTask.prepareCommandString: VARS: {}", variablesMap); + command = StringSubstitutor.replace(command, variablesMap); + log.trace("AbstractRecoveryTask.prepareCommandString: END: {}", command); + return command; + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/EmsClientRecoveryTask.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/EmsClientRecoveryTask.java new file mode 100644 index 0000000..4062e88 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/EmsClientRecoveryTask.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import gr.iccs.imu.ems.common.client.SshClientProperties; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * EMS client Self-Healing + */ +@Slf4j +@Component +public class EmsClientRecoveryTask

extends VmNodeRecoveryTask

{ + @Getter + private final List recoveryCommands = List.of( + new RECOVERY_COMMAND("Initial wait...", + "pwd", 0, 0), + new RECOVERY_COMMAND("Sending baguette client kill command...", + "${BAGUETTE_CLIENT_BASE_DIR}/bin/kill.sh", 0, 2000), + new RECOVERY_COMMAND("Sending baguette client start command...", + "${BAGUETTE_CLIENT_BASE_DIR}/bin/run.sh", 0, 10000) + ); + + public EmsClientRecoveryTask(@NonNull EventBus eventBus, @NonNull PasswordUtil passwordUtil, @NonNull TaskScheduler taskScheduler, @NonNull CollectorContext

collectorContext, @NonNull SelfHealingProperties selfHealingProperties) { + super(eventBus, passwordUtil, taskScheduler, selfHealingProperties, collectorContext); + } + + public void runNodeRecovery(RecoveryContext recoveryContext) throws Exception { + String emsRecoveryFile = selfHealingProperties.getRecovery().getFile().get("baguette"); + log.debug("runNodeRecovery: file={}", emsRecoveryFile); + if (StringUtils.isNotBlank(emsRecoveryFile)) + runNodeRecovery(emsRecoveryFile, recoveryContext); + else + runNodeRecovery(recoveryCommands, recoveryContext); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/NetdataAgentLocalRecoveryTask.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/NetdataAgentLocalRecoveryTask.java new file mode 100644 index 0000000..f6d6ba6 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/NetdataAgentLocalRecoveryTask.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Local Netdata agent Self-Healing + */ +@Slf4j +@Component +public class NetdataAgentLocalRecoveryTask extends ShellRecoveryTask { + @Getter + private final List recoveryCommands = Collections.unmodifiableList(Arrays.asList( + new RECOVERY_COMMAND("Initial wait...", + "pwd", 0, 0), + new RECOVERY_COMMAND("Sending Netdata agent kill command...", + "sudo sh -c 'ps -U netdata -o \"pid\" --no-headers | xargs kill -9' ", 0, 2000), + new RECOVERY_COMMAND("Sending Netdata agent start command...", + "sudo netdata", 0, 10000) + )); + + public NetdataAgentLocalRecoveryTask(@NonNull EventBus eventBus, @NonNull PasswordUtil passwordUtil, @NonNull TaskScheduler taskScheduler, @NonNull SelfHealingProperties selfHealingProperties) { + super(eventBus, passwordUtil, taskScheduler, selfHealingProperties); + } + + public void runNodeRecovery(RecoveryContext recoveryContext) throws Exception { + String netdataRecoveryFile = selfHealingProperties.getRecovery().getFile().get("netdata"); + log.debug("runNodeRecovery: file={}", netdataRecoveryFile); + if (StringUtils.isNotBlank(netdataRecoveryFile)) + runNodeRecovery(netdataRecoveryFile, recoveryContext); + else + runNodeRecovery(recoveryCommands, recoveryContext); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/NetdataAgentRecoveryTask.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/NetdataAgentRecoveryTask.java new file mode 100644 index 0000000..40e1f42 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/NetdataAgentRecoveryTask.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import gr.iccs.imu.ems.common.client.SshClientProperties; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Remote Netdata agent Self-Healing using an SSH connection + */ +@Slf4j +@Component +public class NetdataAgentRecoveryTask

extends VmNodeRecoveryTask

{ + @Getter + private final List recoveryCommands = Collections.unmodifiableList(Arrays.asList( + new RECOVERY_COMMAND("Initial wait...", + "pwd", 0, 0), + new RECOVERY_COMMAND("Sending Netdata agent kill command...", + "sudo sh -c 'ps -U netdata -o \"pid\" --no-headers | xargs kill -9' ", 0, 2000), + new RECOVERY_COMMAND("Sending Netdata agent start command...", + "sudo netdata", 0, 10000) + )); + + public NetdataAgentRecoveryTask(@NonNull EventBus eventBus, @NonNull PasswordUtil passwordUtil, @NonNull TaskScheduler taskScheduler, @NonNull CollectorContext

collectorContext, @NonNull SelfHealingProperties selfHealingProperties) { + super(eventBus, passwordUtil, taskScheduler, selfHealingProperties, collectorContext); + } + + public void runNodeRecovery(RecoveryContext recoveryContext) throws Exception { + String netdataRecoveryFile = selfHealingProperties.getRecovery().getFile().get("netdata"); + log.debug("runNodeRecovery: file={}", netdataRecoveryFile); + if (StringUtils.isNotBlank(netdataRecoveryFile)) + runNodeRecovery(netdataRecoveryFile, recoveryContext); + else + runNodeRecovery(recoveryCommands, recoveryContext); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RECOVERY_COMMAND.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RECOVERY_COMMAND.java new file mode 100644 index 0000000..f218af4 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RECOVERY_COMMAND.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import lombok.Data; + +@Data +public class RECOVERY_COMMAND { + private final String name; + private final String command; + private final long waitBefore; + private final long waitAfter; +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryConstant.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryConstant.java new file mode 100644 index 0000000..f340471 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryConstant.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +/** + * Recovery Constant + */ +public class RecoveryConstant { + public final static String SELF_HEALING_RECOVERY_STARTED = "SELF_HEALING_RECOVERY_STARTED"; + public final static String SELF_HEALING_RECOVERY_FAILED = "SELF_HEALING_RECOVERY_FAILED"; + public final static String SELF_HEALING_RECOVERY_GIVE_UP = "SELF_HEALING_RECOVERY_GIVE_UP"; + public final static String SELF_HEALING_RECOVERY_COMPLETED = "SELF_HEALING_RECOVERY_COMPLETED"; +} \ No newline at end of file diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryContext.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryContext.java new file mode 100644 index 0000000..88895ef --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryContext.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Getter +@Service +@ToString +public class RecoveryContext { + private final static List variablesToRetrieve = List.of( + "BAGUETTE_CLIENT_BASE_DIR:baseDir", "BAGUETTE_CLIENT_BASE_DIR:getBaseDir()"); + + private final Map variablesMap = new HashMap<>(); + + public void initialize(@NonNull Object... sources) { + for (Object source : sources) { + log.trace("RecoveryContext.initialize: Processing source: {}", source); + initialize(source); + } + } + + public void initialize(@NonNull Object source) { + log.debug("RecoveryContext.initialize: BEGIN: source: {}", source); + try { + log.trace("RecoveryContext.initialize: variablesToRetrieve: {}", variablesToRetrieve); + Map vars = new HashMap<>(); + for (String varSpec : variablesToRetrieve) { + log.trace("RecoveryContext.initialize: var-spec={}", varSpec); + boolean isMethod = varSpec.endsWith("()"); + varSpec = isMethod ? varSpec.substring(0, varSpec.length()-2) : varSpec; + + String[] s = varSpec.split(":", 2); + String entryName = s[0]; + String varName = s.length==2 ? s[1] : s[0]; + log.trace("RecoveryContext.initialize: is-method={}, var-name={}, entry-name={}", isMethod, varName, entryName); + + try { + Object varValue; + if (isMethod) { + log.trace("RecoveryContext.initialize: Retrieving Method: {}", varName); + Method method = source.getClass().getMethod(varName); + log.trace("RecoveryContext.initialize: Method: {}", method); + varValue = method.invoke(source); + } else { + log.trace("RecoveryContext.initialize: Retrieving Field: {}", varName); + Field field = source.getClass().getField(varName); + log.trace("RecoveryContext.initialize: Field: {}", field); + varValue = field.get(source); + } + log.trace("RecoveryContext.initialize: Var-value: {} = {}", varName, varValue); + if (varValue != null) + vars.put(entryName, varValue.toString()); + } catch (NoSuchFieldException | NoSuchMethodException e) { + log.trace("RecoveryContext.initialize: Method or Field not found or not accessible: {} -- Exception: ", varName, e); + } + } + log.debug("RecoveryContext.initialize: Variables collected: {}", vars); + + log.trace("RecoveryContext.initialize: Variables map BEFORE update: {}", variablesMap); + variablesMap.putAll(vars); + log.trace("RecoveryContext.initialize: Variables map AFTER update: {}", variablesMap); + + log.debug("RecoveryContext.initialize: END"); + } catch (Exception e) { + log.error("RecoveryContext.initialize: EXCEPTION: Source={}\n", source, e); + } + } +} \ No newline at end of file diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryTask.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryTask.java new file mode 100644 index 0000000..22b3482 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/RecoveryTask.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.FileReader; +import java.lang.reflect.Type; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +/** + * Self-Healing task + */ +public interface RecoveryTask { + Map getNodeInfo(); + void setNodeInfo(Map nodeInfo); + + List getRecoveryCommands(); + + void runNodeRecovery(RecoveryContext context) throws Exception; + + void runNodeRecovery(List recoveryCommandsList, RecoveryContext context) throws Exception; + + default void runNodeRecovery(String recoveryCommandsFile, RecoveryContext context) throws Exception { + try (FileReader reader = new FileReader(Paths.get(recoveryCommandsFile).toFile())) { + Type listType = new TypeToken>(){}.getType(); + List recoveryCommandsList = new Gson().fromJson(reader, listType); + runNodeRecovery(recoveryCommandsList, context); + } + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/SelfHealingProperties.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/SelfHealingProperties.java new file mode 100644 index 0000000..2f9174e --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/SelfHealingProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Data +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "self.healing") +public class SelfHealingProperties implements InitializingBean { + private boolean enabled = true; + private Recovery recovery = new Recovery(); + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("SelfHealingProperties: {}", this); + } + + @Data + public static class Recovery { + private long delay = 1000; + private long retryDelay = 60000; + private int maxRetries = 3; + + private Map file = new HashMap<>(); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/ShellRecoveryTask.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/ShellRecoveryTask.java new file mode 100644 index 0000000..2ef23f7 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/ShellRecoveryTask.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static gr.iccs.imu.ems.common.recovery.RecoveryConstant.SELF_HEALING_RECOVERY_STARTED; +import static gr.iccs.imu.ems.common.recovery.RecoveryConstant.SELF_HEALING_RECOVERY_COMPLETED; + +/** + * Local-node Self-Healing using Shell + */ +@Slf4j +@Component +public class ShellRecoveryTask extends AbstractRecoveryTask { + public ShellRecoveryTask(EventBus eventBus, PasswordUtil passwordUtil, TaskScheduler taskScheduler, SelfHealingProperties selfHealingProperties) { + super(eventBus, passwordUtil, taskScheduler, selfHealingProperties); + } + + @SneakyThrows + public List getRecoveryCommands() { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery(RecoveryContext recoveryContext) throws Exception { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery(List recoveryCommands, RecoveryContext recoveryContext) throws Exception { + log.debug("ShellRecoveryTask: runNodeRecovery(): node-info={}", nodeInfo); + + // Send recovery start event + eventBus.send(SELF_HEALING_RECOVERY_STARTED, ""); + + // Carrying out recovery commands + log.info("ShellRecoveryTask: runNodeRecovery(): Executing {} recovery commands", recoveryCommands.size()); + for (RECOVERY_COMMAND command : recoveryCommands) { + if (command==null || StringUtils.isBlank(command.getCommand())) continue; + + waitFor(command.getWaitBefore(), command.getName()); + + // Run command as a local process + String commandString = prepareCommandString(command.getCommand(), recoveryContext); + log.warn("############## {}...", command.getName()); + log.warn("############## Command: {}", commandString); + Process process = Runtime.getRuntime().exec(commandString); + + // Redirect SSH output to standard output + final AtomicBoolean closed = new AtomicBoolean(false); + redirectShellOutput(process.getInputStream(), "OUT", closed); + redirectShellOutput(process.getErrorStream(), "ERR", closed); + + waitFor(command.getWaitAfter(), command.getName()); + + closed.set(true); + //if (process.isAlive()) process.destroyForcibly(); + } + log.info("ShellRecoveryTask: runNodeRecovery(): Executed {} recovery commands", recoveryCommands.size()); + + // Send recovery complete event + eventBus.send(SELF_HEALING_RECOVERY_COMPLETED, ""); + } + + private void redirectShellOutput(InputStream in, String id, AtomicBoolean closed) { + redirectOutput(in, id, closed, + "ShellRecoveryTask: redirectShellOutput(): Connection closed: id={}", + "ShellRecoveryTask: redirectShellOutput(): Exception while copying Process IN stream: id={}\n"); + //IoUtils.copy(in, System.out); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/VmNodeRecoveryTask.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/VmNodeRecoveryTask.java new file mode 100644 index 0000000..d35b8d1 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/recovery/VmNodeRecoveryTask.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.recovery; + +import gr.iccs.imu.ems.common.client.SshClient; +import gr.iccs.imu.ems.common.client.SshClientProperties; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static gr.iccs.imu.ems.common.recovery.RecoveryConstant.SELF_HEALING_RECOVERY_COMPLETED; + +/** + * VM-node Self-Healing using an SSH connection + */ +@Slf4j +@Component +public class VmNodeRecoveryTask

extends AbstractRecoveryTask { + @NonNull private final CollectorContext

collectorContext; + + private P sshClientProperties; + + public VmNodeRecoveryTask(EventBus eventBus, PasswordUtil passwordUtil, TaskScheduler taskScheduler, SelfHealingProperties selfHealingProperties, CollectorContext

collectorContext) { + super(eventBus, passwordUtil, taskScheduler, selfHealingProperties); + this.collectorContext = collectorContext; + } + + public void setNodeInfo(@NonNull Map nodeInfo) { + super.setNodeInfo(nodeInfo); + this.sshClientProperties = createSshClientProperties(); + } + + @SneakyThrows + public List getRecoveryCommands() { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery(RecoveryContext recoveryContext) throws Exception { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery(List recoveryCommands, RecoveryContext recoveryContext) throws Exception { + log.debug("VmNodeRecoveryTask: runNodeRecovery(): BEGIN: recovery-command: {}", recoveryCommands); + + // Connect to Node (VM) + SshClient sshc = connectToNode(); + + // Redirect SSH output to standard output + final AtomicBoolean closed = new AtomicBoolean(false); + redirectSshOutput(sshc.getIn(), "OUT", closed); + + // Carrying out recovery commands + log.info("VmNodeRecoveryTask: runNodeRecovery(): Executing {} recovery commands", recoveryCommands.size()); + for (RECOVERY_COMMAND command : recoveryCommands) { + if (command==null || StringUtils.isBlank(command.getCommand())) continue; + + waitFor(command.getWaitBefore(), command.getName()); + + // Send command to node for execution + String commandString = prepareCommandString(command.getCommand(), recoveryContext); + log.warn("############## {}...", command.getName()); + log.warn("############## Command: {}", commandString); + sshc.getOut().println(commandString); + waitFor(command.getWaitAfter(), command.getName()); + } + log.info("VmNodeRecoveryTask: runNodeRecovery(): Executed {} recovery commands", recoveryCommands.size()); + + // Disconnect from node + disconnectFromNode(sshc, closed); + + // Send recovery complete event + eventBus.send(SELF_HEALING_RECOVERY_COMPLETED, sshClientProperties.getServerAddress()); + } + + private String str(Object o) { + if (o==null) return ""; + return o.toString(); + } + + private P createSshClientProperties() { + log.debug("VmNodeRecoveryTask: createSshClientProperties(): BEGIN:"); + + // Extract connection info and credentials + String os = str(nodeInfo.get("operatingSystem")); + String address = str(nodeInfo.get("address")); + String type = str(nodeInfo.get("type")); + String portStr = str(nodeInfo.get("ssh.port")); + String username = str(nodeInfo.get("ssh.username")); + String password = str(nodeInfo.get("ssh.password")); + String key = str(nodeInfo.get("ssh.key")); + String fingerprint = str(nodeInfo.get("ssh.fingerprint")); + String keyAlgorithm = str(nodeInfo.get("ssh.key-algorithm")); + String keyFormat = str(nodeInfo.get("ssh.key-format")); + int port = 22; + try { + if (StringUtils.isNotBlank(portStr)) + port = Integer.parseInt(portStr); + if (port<1 || port>65535) + port = 22; + } catch (Exception ignored) {} + + log.debug("VmNodeRecoveryTask: createSshClientProperties(): os={}, address={}, type={}", os, address, type); + log.debug("VmNodeRecoveryTask: createSshClientProperties(): username={}, password={}", username, passwordUtil.encodePassword(password)); + log.debug("VmNodeRecoveryTask: createSshClientProperties(): fingerprint={}, key={}", fingerprint, passwordUtil.encodePassword(key)); + + // Connect to node and restart EMS client + P config = collectorContext.getSshClientProperties(); + config.setServerAddress(address); + config.setServerPort(port); + config.setServerUsername(username); + if (!password.isEmpty()) { + config.setServerPassword(password); + } + if (!key.isEmpty()) { + config.setServerPubkey(key); + config.setServerPubkeyFingerprint(fingerprint); + config.setServerPubkeyAlgorithm(keyAlgorithm); + config.setServerPubkeyFormat(keyFormat); + } + + //XXX:TODO: Make recovery authTimeout configurable + config.setAuthTimeout(60000); + + return config; + } + + private SshClient

connectToNode() throws IOException { + SshClient

sshc = collectorContext.getSshClient(); + sshc.setConfiguration(sshClientProperties); + //XXX:TODO: Try enabling server key verification + sshc.setUseServerKeyVerifier(false); + log.info("VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address={}, port={}, username={}", + sshClientProperties.getServerAddress(), sshClientProperties.getServerPort(), sshClientProperties.getServerUsername()); + sshc.start(); + log.debug("VmNodeRecoveryTask: connectToNode(): Connected to node: address={}, port={}, username={}", + sshClientProperties.getServerAddress(), sshClientProperties.getServerPort(), sshClientProperties.getServerUsername()); + return sshc; + } + + private void disconnectFromNode(SshClient sshc, AtomicBoolean closed) throws IOException { + log.info("VmNodeRecoveryTask: disconnectFromNode(): Disconnecting from node: address={}, port={}, username={}", + sshClientProperties.getServerAddress(), sshClientProperties.getServerPort(), sshClientProperties.getServerUsername()); + closed.set(true); + sshc.stop(); + log.debug("VmNodeRecoveryTask: disconnectFromNode(): Disconnected from node: address={}, port={}, username={}", + sshClientProperties.getServerAddress(), sshClientProperties.getServerPort(), sshClientProperties.getServerUsername()); + } + + private void redirectSshOutput(InputStream in, String id, AtomicBoolean closed) { + redirectOutput(in, id, closed, + "VmNodeRecoveryTask: redirectSshOutput(): Connection closed: id={}", + "VmNodeRecoveryTask: redirectSshOutput(): Exception while copying SSH IN stream: id={}\n"); + //IoUtils.copy(sshc.getIn(), System.out); + } +} diff --git a/ems-core/common/src/main/java/gr/iccs/imu/ems/common/selfhealing/SelfHealingManager.java b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/selfhealing/SelfHealingManager.java new file mode 100644 index 0000000..468be96 --- /dev/null +++ b/ems-core/common/src/main/java/gr/iccs/imu/ems/common/selfhealing/SelfHealingManager.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.common.selfhealing; + +import java.util.Collection; + +public interface SelfHealingManager { + enum MODE { ALL, INCLUDED, EXCLUDED } + enum NODE_STATE {NOT_MONITORED, UNKNOWN, OK, UP, ERROR, DOWN, RECOVERING } + + boolean isEnabled(); + void setEnabled(boolean b); + + MODE getMode(); + void setMode(MODE mode); + + Collection getNodes(); + boolean containsNode(T node); + boolean containsAny(Collection nodes); + boolean isMonitored(T node); + void addNode(T node); + void addAllNodes(Collection nodes); + void removeNode(T node); + void removeAllNodes(Collection nodes); + void clear(); + + NODE_STATE getNodeSelfHealingState(T node); + String getNodeSelfHealingStateText(T node); + default void setNodeSelfHealingState(T node, NODE_STATE state) { + setNodeSelfHealingState(node, state, null); + } + void setNodeSelfHealingState(T node, NODE_STATE state, String text); +} diff --git a/ems-core/common/src/main/resources/META-INF/spring.factories b/ems-core/common/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..5268298 --- /dev/null +++ b/ems-core/common/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=plugin.gr.iccs.imu.ems.common.PluginManager \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/baguette-skip.yml b/ems-core/config-files/baguette-client-install/linux-yaml/baguette-skip.yml new file mode 100644 index 0000000..c1433d4 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/baguette-skip.yml @@ -0,0 +1,33 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set executed when Baguette client is not installed +# (in the case of Resource-Limited nodes) +# + +--- +os: LINUX +description: EMS client SKIP installation instruction set +condition: >- + ${SKIP_BAGUETTE_INSTALLATION:-false} || + '${OS_ARCHITECTURE:-x}'.startsWith('arm') || + ${CPU_PROCESSORS:-0} <= ${BAGUETTE_INSTALLATION_MIN_PROCESSORS:-0} || + ${RAM_AVAILABLE_KB:-0} <= ${BAGUETTE_INSTALLATION_MIN_RAM:-0} || + ${DISK_FREE_KB:-0} <= ${BAGUETTE_INSTALLATION_MIN_DISK_FREE:-0} +instructions: + - description: 'DEBUG: Print node pre-registration VARIABLES' + taskType: PRINT_VARS + - description: Set __EMS_CLIENT_INSTALL__ variable + taskType: SET_VARS + variables: + __EMS_CLIENT_INSTALL__: SKIPPED + - description: Log SKIP installation + taskType: LOG + message: EMS client installation SKIPPED at Node diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/baguette.yml b/ems-core/config-files/baguette-client-install/linux-yaml/baguette.yml new file mode 100644 index 0000000..0caa8cc --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/baguette.yml @@ -0,0 +1,133 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set for installing Baguette (ems) client in a node +# + +--- +os: LINUX +description: EMS client installation instruction set at VM node +condition: >- + ! ${SKIP_BAGUETTE_INSTALLATION:-false} && + ! '${OS_ARCHITECTURE:-x}'.startsWith('arm') && + ${CPU_PROCESSORS:-0} > ${BAGUETTE_INSTALLATION_MIN_PROCESSORS:-0} && + ${RAM_AVAILABLE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_RAM:-0} && + ${DISK_FREE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_DISK_FREE:-0} +instructions: + - description: 'DEBUG: Print node pre-registration VARIABLES' + taskType: PRINT_VARS + - description: Check if 'java' is installed at Node + taskType: CHECK + command: '${BAGUETTE_CLIENT_BASE_DIR}/jre8/bin/java -version' + executable: false + exitCode: 0 + match: false + message: Java is not installed at Node + - description: Check if EMS client is already installed at Node + taskType: CHECK + command: '[[ -f ${BAGUETTE_CLIENT_BASE_DIR}/conf/ok.txt ]] && exit 99' + executable: false + exitCode: 99 + match: true + message: EMS client is already installed at Node + - description: '-- LIST ${BAGUETTE_CLIENT_BASE_DIR}/.. BEFORE --' + taskType: CMD + command: 'ls -l ${BAGUETTE_CLIENT_BASE_DIR}/.. ' + executable: false + exitCode: 0 + match: false + - description: Log EMS client installation start + taskType: LOG + message: Starting EMS client installation at Node + - description: Upload EMS client installation package + taskType: COPY + fileName: /tmp/baguette-client.tgz + localFileName: '${EMS_PUBLIC_DIR}/resources/baguette-client.tgz' + executable: false + exitCode: 0 + match: false + - description: Upload installation package MD5 checksum + taskType: COPY + fileName: /tmp/baguette-client.tgz.md5 + localFileName: '${EMS_PUBLIC_DIR}/resources/baguette-client.tgz.md5' + executable: false + exitCode: 0 + match: false + - description: Check MD5 checksum of installation package + taskType: CHECK + command: >- + [[ `cat /tmp/baguette-client.tgz.md5` != `md5sum /tmp/baguette-client.tgz | cut -d ' ' -f 1 ` ]] && exit 99 + executable: false + exitCode: 99 + match: true + - description: Extract installation package to target folder + taskType: CMD + command: >- + ${ROOT_CMD} tar zxvf /tmp/baguette-client.tgz -C ${BAGUETTE_CLIENT_BASE_DIR}/../ + executable: false + exitCode: 0 + match: false + executionTimeout: 120000 + - description: Change files and folders ownership + taskType: CMD + command: '${ROOT_CMD} chown -R ${NODE_SSH_USERNAME} ${BAGUETTE_CLIENT_BASE_DIR}' + executable: false + exitCode: 0 + match: false + - description: Touch files + taskType: CMD + command: 'touch ${BAGUETTE_CLIENT_BASE_DIR}/logs/output.txt' + executable: false + exitCode: 0 + match: false + - description: Create conf directory + taskType: CMD + command: 'mkdir ${BAGUETTE_CLIENT_BASE_DIR}/conf/' + executable: false + exitCode: 0 + match: false + - description: Copy-and-process configuration to target + taskType: FILE + localFileName: '${EMS_CONFIG_DIR}/baguette-client/' + fileName: '${BAGUETTE_CLIENT_BASE_DIR}' + executable: false + exitCode: 0 + match: false + - description: Clean installation package from /tmp + taskType: CMD + command: rm -f /tmp/baguette-client.tgz* + executable: false + exitCode: 0 + match: false + - description: Write success file + taskType: CMD + command: 'echo SUCCESS >> ${BAGUETTE_CLIENT_BASE_DIR}/conf/ok.txt' + executable: false + exitCode: 0 + match: false + - description: '-- LIST ${BAGUETTE_CLIENT_BASE_DIR}/.. AFTER --' + taskType: CMD + command: 'ls -l ${BAGUETTE_CLIENT_BASE_DIR}/.. ' + executable: false + exitCode: 0 + match: false + - description: '-- LIST baguette-client FILES --' + taskType: CMD + command: 'ls -l ${BAGUETTE_CLIENT_BASE_DIR} ' + executable: false + exitCode: 0 + match: false + - description: Set __EMS_CLIENT_INSTALL__ variable + taskType: SET_VARS + variables: + __EMS_CLIENT_INSTALL__: INSTALLED + - description: Log installation end + taskType: LOG + message: EMS client installation completed at Node diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/check-ignore.yml b/ems-core/config-files/baguette-client-install/linux-yaml/check-ignore.yml new file mode 100644 index 0000000..7ec210f --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/check-ignore.yml @@ -0,0 +1,35 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set to check if Node must be ignored by EMS +# Nothing will be installed by EMS in the node +# (Checks if file '/tmp/.EMS_IGNORE_NODE' exists) +# + +--- +os: LINUX +description: Check if node must be ignored +condition: '! ${SKIP_IGNORE_CHECK:-false}' +instructions: + - description: Checking for .EMS_IGNORE_NODE file... + taskType: LOG + message: Checking for .EMS_IGNORE_NODE file... + - description: Checking for .EMS_IGNORE_NODE file + taskType: CHECK + command: test -e /tmp/.EMS_IGNORE_NODE + executable: false + exitCode: 0 + match: false + - description: Set __EMS_IGNORE_NODE__ variable + taskType: SET_VARS + variables: + __EMS_IGNORE_NODE__: IGNORED + - description: Stop further processing + taskType: EXIT diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/detect.yml b/ems-core/config-files/baguette-client-install/linux-yaml/detect.yml new file mode 100644 index 0000000..d48e9a2 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/detect.yml @@ -0,0 +1,83 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set for detecting the node size (cores, ram, storage etc) +# + +--- +os: LINUX +description: 'Detect node features (OS, architecture, cores, RAM, disk etc)' +condition: '! ${SKIP_DETECTION:-false}' +instructions: + - description: Detecting target node type... + taskType: LOG + message: Detecting target node type... + - description: Copying detection script to node... + taskType: COPY + fileName: /tmp/detect.sh + localFileName: bin/detect.sh + executable: false + exitCode: 0 + match: false + - description: Make detection script executable + taskType: CMD + command: 'chmod +x /tmp/detect.sh ' + executable: false + exitCode: 0 + match: false + - description: Run detection script + taskType: CMD +# command: '/tmp/detect.sh &> /tmp/detect.txt' + command: 'if [ ! -e /tmp/detect.txt ]; then /tmp/detect.sh &> /tmp/detect.txt; fi' + executable: false + exitCode: 0 + match: false + - description: Copying detection results back to EMS server... + taskType: DOWNLOAD + fileName: /tmp/detect.txt + localFileName: 'logs/detect.${NODE_ADDRESS}--${TIMESTAMP-FILE}.txt' + executable: false + exitCode: 0 + match: false + patterns: + CPU_SOCKETS: '^\s*CPU_SOCKETS\s*[=:]\s*(.*)\s*' + CPU_CORES: '^\s*CPU_CORES\s*[=:]\s*(.*)\s*' + CPU_PROCESSORS: '^\s*CPU_PROCESSORS\s*[=:]\s*(.*)\s*' + RAM_TOTAL_KB: '^\s*RAM_TOTAL_KB\s*[=:]\s*(.*)\s*' + RAM_AVAILABLE_KB: '^\s*RAM_AVAILABLE_KB\s*[=:]\s*(.*)\s*' + RAM_FREE_KB: '^\s*RAM_FREE_KB\s*[=:]\s*(.*)\s*' + RAM_USED_KB: '^\s*RAM_USED_KB\s*[=:]\s*(.*)\s*' + RAM_UTILIZATION: '^\s*RAM_UTILIZATION\s*[=:]\s*(.*)\s*' + DISK_TOTAL_KB: '^\s*DISK_TOTAL_KB\s*[=:]\s*(.*)\s*' + DISK_FREE_KB: '^\s*DISK_FREE_KB\s*[=:]\s*(.*)\s*' + DISK_USED_KB: '^\s*DISK_USED_KB\s*[=:]\s*(.*)\s*' + DISK_UTILIZATION: '^\s*DISK_UTILIZATION\s*[=:]\s*(.*)\s*' + OS_ARCHITECTURE: '^\s*OS_ARCHITECTURE\s*[=:]\s*(.*)\s*' + OS_KERNEL: '^\s*OS_KERNEL\s*[=:]\s*(.*)\s*' + OS_KERNEL_RELEASE: '^\s*OS_KERNEL_RELEASE\s*[=:]\s*(.*)\s*' + - description: Detection results... + taskType: LOG + message: |- + Detection results: + CPU_SOCKETS=${CPU_SOCKETS:-na} + CPU_CORES=${CPU_CORES:-na} + CPU_PROCESSORS=${CPU_PROCESSORS:-na} + RAM_TOTAL_KB=${RAM_TOTAL_KB:-na} + RAM_AVAILABLE_KB=${RAM_AVAILABLE_KB:-na} + RAM_FREE_KB=${RAM_FREE_KB:-na} + RAM_USED_KB=${RAM_USED_KB:-na} + RAM_UTILIZATION=${RAM_UTILIZATION:-na} + DISK_TOTAL_KB=${DISK_TOTAL_KB:-na} + DISK_FREE_KB=${DISK_FREE_KB:-na} + DISK_USED_KB=${DISK_USED_KB:-na} + DISK_UTILIZATION=${DISK_UTILIZATION:-na} + OS_ARCHITECTURE=${OS_ARCHITECTURE:-na} + OS_KERNEL=${OS_KERNEL:-na} + OS_KERNEL_RELEASE=${OS_KERNEL_RELEASE:-na} diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/jre8.yml b/ems-core/config-files/baguette-client-install/linux-yaml/jre8.yml new file mode 100644 index 0000000..c58c05f --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/jre8.yml @@ -0,0 +1,79 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set for installing JRE +# + +--- +os: LINUX +description: JRE 8u282 installation instruction set at VM node +condition: >- + ! ${SKIP_JRE_INSTALLATION:-false} && + ! '${OS_ARCHITECTURE:-x}'.startsWith('arm') && + ${CPU_PROCESSORS:-0} > ${BAGUETTE_INSTALLATION_MIN_PROCESSORS:-0} && + ${RAM_AVAILABLE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_RAM:-0} && + ${DISK_FREE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_DISK_FREE:-0} +instructions: + - description: Check if JRE 8u282 is already installed at Node + taskType: CHECK + command: '[[ -f ${BAGUETTE_CLIENT_BASE_DIR}/jre8/bin/java ]] && exit 99' + executable: false + exitCode: 99 + match: true + message: JRE 8u282 is already installed at Node + - description: Install JRE 8u282... + taskType: LOG + message: Install JRE 8u282... + - description: Mkdir Baguette Client installation folder + taskType: CMD + command: '${ROOT_CMD} mkdir -p ${BAGUETTE_CLIENT_BASE_DIR} ' + executable: false + exitCode: 0 + match: false + executionTimeout: 120000 +# - description: Download JRE package +# taskType: CMD +# command: >- +# curl -k ${DOWNLOAD_URL}/resources/zulu8.52.0.23-ca-jre8.0.282-linux_x64.tar.gz --output /tmp/jre8.282.tar.gz +# executable: false +# exitCode: 0 +# match: false + - description: Copy JRE package + taskType: COPY + fileName: /tmp/jre8.282.tar.gz + localFileName: '${EMS_PUBLIC_DIR}/resources/${JRE8_LINUX_X64_PACKAGE}' + executable: false + exitCode: 0 + match: false + - description: Extract JRE package into installation folder + taskType: CMD + command: '${ROOT_CMD} tar zxvf /tmp/jre8.282.tar.gz -C ${BAGUETTE_CLIENT_BASE_DIR}' + executable: false + exitCode: 0 + match: false + - description: Rename JRE directory + taskType: CMD + command: >- + ${ROOT_CMD} mv ${BAGUETTE_CLIENT_BASE_DIR}/zulu* ${BAGUETTE_CLIENT_BASE_DIR}/jre8 + executable: false + exitCode: 0 + match: false + - description: List BC home directory + taskType: CMD + command: 'ls -l ${BAGUETTE_CLIENT_BASE_DIR}' + executable: false + exitCode: 0 + match: false + - description: Print JRE version + taskType: CMD + command: '${BAGUETTE_CLIENT_BASE_DIR}/jre8/bin/java -version' + executable: false + exitCode: 0 + match: false diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/netdata.yml b/ems-core/config-files/baguette-client-install/linux-yaml/netdata.yml new file mode 100644 index 0000000..b368069 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/netdata.yml @@ -0,0 +1,61 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set for installing Netdata agent +# + +--- +os: 'LINUX' +description: Netdata installation instruction set at VM node +condition: '! ${SKIP_NETDATA_INSTALLATION:-false}' +instructions: + - description: Log Netdata installation start + taskType: LOG + message: Starting Netdata installation at Node + - description: Check if Netdata is already installed at Node + taskType: CHECK +# command: '[ $(ps -e -o pid,comm,cgroup |grep netdata |grep -v docker |grep -v lxc |wc -l) -gt 0 ] && exit 99' + command: '[[ -f /usr/sbin/netdata ]] && exit 99' + executable: false + exitCode: 99 + match: true + message: Netdata is already installed at Node + - description: Log Wait if apt is being updated + taskType: LOG + message: Wait if apt is being updated + - description: Start unattended-upgrade if available + taskType: CMD + command: >- + if command -v unattended-upgrade &> /dev/null ; + then unattended-upgrade -d ; + else echo "Command 'unattended-upgrade' is not available" ; + fi + executionTimeout: 600000 + - description: Wait if apt is being updated + taskType: CMD + command: >- + while [ `ps aux | grep -i lock_is_held | grep -v grep | wc -l` != 0 ]; do + echo "Lock_is_held..."; ps aux | grep -i lock_is_held ; sleep 10 ; + done + executionTimeout: 600000 + - description: Download Netdata kickstart.sh + taskType: CMD + command: >- + curl https://my-netdata.io/kickstart-static64.sh > /tmp/netdata-kickstart.sh + executionTimeout: 600000 + - description: Make Netdata kickstart.sh executable + taskType: CMD + command: chmod +x /tmp/netdata-kickstart.sh + executionTimeout: 600000 + - description: Run Netdata kickstart.sh + taskType: CMD + command: >- + /tmp/netdata-kickstart.sh --dont-wait --no-updates --disable-telemetry --dont-start-it --stable-channel --disable-cloud + executionTimeout: 600000 diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/recover-baguette.yml b/ems-core/config-files/baguette-client-install/linux-yaml/recover-baguette.yml new file mode 100644 index 0000000..d84d1a4 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/recover-baguette.yml @@ -0,0 +1,31 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set for recovering Baguette client +# + +--- +os: LINUX +description: Restarting Baguette agent at VM node +instructions: + - description: Killing previous EMS client process + taskType: CMD + command: '${BAGUETTE_CLIENT_BASE_DIR}/bin/kill.sh' + executable: false + exitCode: 0 + match: false + retries: 5 + - description: Starting new EMS client process + taskType: CMD + command: '${BAGUETTE_CLIENT_BASE_DIR}/bin/run.sh' + executable: false + exitCode: 0 + match: false + retries: 5 diff --git a/ems-core/config-files/baguette-client-install/linux-yaml/start-agents.yml b/ems-core/config-files/baguette-client-install/linux-yaml/start-agents.yml new file mode 100644 index 0000000..a009881 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux-yaml/start-agents.yml @@ -0,0 +1,54 @@ +# +# Copyright (C) 2017-2022 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# +# Instructions Set for starting Agents: +# Baguette client, and Netdata +# + +--- +os: LINUX +description: "Starting Netdata and Baguette agents at VM node" +condition: "! ${SKIP_START:-false}" +instructions: + - description: "Launch EMS client" + taskType: CMD + command: "${BAGUETTE_CLIENT_BASE_DIR}/bin/run.sh" + executable: false + exitCode: 0 + match: false + retries: 5 + - description: "Check if Netdata is already running" + taskType: CHECK + #command: "[[ $(( `ps -ef |grep /usr/sbin/netdata |grep -v grep |wc -l`+1 )) -gt 1 ]] && exit 1 || exit 0" + command: "[[ $(ps -e -o pid,comm,cgroup |grep netdata |grep -v grep |grep -v docker |grep -v lxc |wc -l) -gt 0 ]] && exit 1 || exit 0" + executable: false + exitCode: 1 + match: true + message: "Netdata is already running" + - description: "Copy Netdata Prometheus plugin configuration to node's /tmp directory" + taskType: FILE + localFileName: "${EMS_CONFIG_DIR}/baguette-client-install/netdata/go.d/prometheus.conf" + fileName: "/tmp" + executable: false + exitCode: 0 + match: false + - description: "Move prometheus config from /tmp to /etc/netdata/go.d/ directory" + taskType: CMD + command: "echo ${NODE_SSH_PASSWORD} | sudo -- sh -c 'mkdir -p /etc/netdata/go.d/ && mv -f /tmp/prometheus.conf /etc/netdata/go.d/' " + executable: false + exitCode: 0 + match: false + - description: "Launch Netdata" + taskType: CMD + command: "echo ${NODE_SSH_PASSWORD} | sudo -S -- sh -c '/opt/netdata/bin/netdata || /usr/sbin/netdata || netdata' " + executable: false + exitCode: 0 + match: false + retries: 5 diff --git a/ems-core/config-files/baguette-client-install/linux/baguette-remove.json b/ems-core/config-files/baguette-client-install/linux/baguette-remove.json new file mode 100644 index 0000000..d353ce7 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/baguette-remove.json @@ -0,0 +1,33 @@ +{ + "os": "LINUX", + "description": "EMS client removal instruction set", + "instructions": [ + { + "description": "Kill EMS client if still running", + "taskType": "LOG", + "message": "Killing EMS client if still running..." + }, + { + "description": "Killing previous EMS client process", + "taskType": "CMD", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/kill.sh" + }, + + { + "description": "Rename EMS client folder", + "taskType": "LOG", + "message": "Renaming EMS client folder..." + }, + { + "description": "Renaming EMS client folder if any", + "taskType": "CMD", + "command": "mv ${BAGUETTE_CLIENT_BASE_DIR}/ ${BAGUETTE_CLIENT_BASE_DIR}--$(date +%s)/" + }, + + { + "description": "Log EMS client removal", + "taskType": "LOG", + "message": "EMS client removed from Node" + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/baguette-skip.json b/ems-core/config-files/baguette-client-install/linux/baguette-skip.json new file mode 100644 index 0000000..2941719 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/baguette-skip.json @@ -0,0 +1,23 @@ +{ + "os": "LINUX", + "description": "EMS client SKIP installation instruction set", + "condition": "${SKIP_BAGUETTE_INSTALLATION:-false} || '${OS_ARCHITECTURE:-x}'.startsWith('arm') || ${CPU_PROCESSORS:-0} <= ${BAGUETTE_INSTALLATION_MIN_PROCESSORS:-0} || ${RAM_AVAILABLE_KB:-0} <= ${BAGUETTE_INSTALLATION_MIN_RAM:-0} || ${DISK_FREE_KB:-0} <= ${BAGUETTE_INSTALLATION_MIN_DISK_FREE:-0}", + "instructions": [ + { + "description": "DEBUG: Print node pre-registration VARIABLES", + "taskType": "PRINT_VARS" + }, + { + "description": "Set __EMS_CLIENT_INSTALL__ variable", + "taskType": "SET_VARS", + "variables": { + "__EMS_CLIENT_INSTALL__": "SKIPPED" + } + }, + { + "description": "Log SKIP installation", + "taskType": "LOG", + "message": "EMS client installation SKIPPED at Node" + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/baguette.json b/ems-core/config-files/baguette-client-install/linux/baguette.json new file mode 100644 index 0000000..5c1905d --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/baguette.json @@ -0,0 +1,154 @@ +{ + "os": "LINUX", + "description": "EMS client installation instruction set at VM node", + "condition": "! ${SKIP_BAGUETTE_INSTALLATION:-false} && ! '${OS_ARCHITECTURE:-x}'.startsWith('arm') && ${CPU_PROCESSORS:-0} > ${BAGUETTE_INSTALLATION_MIN_PROCESSORS:-0} && ${RAM_AVAILABLE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_RAM:-0} && ${DISK_FREE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_DISK_FREE:-0}", + "instructions": [ + { + "description": "DEBUG: Print node pre-registration VARIABLES", + "taskType": "PRINT_VARS" + }, + { + "description": "Check if 'java' is installed at Node", + "taskType": "CHECK", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/jre/bin/java -version", + "executable": false, + "exitCode": 0, + "match": false, + "message": "Java is not installed at Node" + }, + { + "description": "Check if EMS client is already installed at Node", + "taskType": "CHECK", + "command": "[[ -f ${BAGUETTE_CLIENT_BASE_DIR}/conf/ok.txt ]] && exit 99", + "executable": false, + "exitCode": 99, + "match": true, + "message": "====== EMS client is already installed at Node ======" + }, + { + "description": "-- LIST ${BAGUETTE_CLIENT_BASE_DIR}/.. BEFORE --", + "taskType": "CMD", + "command": "ls -l ${BAGUETTE_CLIENT_BASE_DIR}/.. ", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Log EMS client installation start", + "taskType": "LOG", + "message": "Starting EMS client installation at Node" + }, + { + "description": "Upload EMS client installation package", + "taskType": "COPY", + "fileName": "/tmp/baguette-client.tgz", + "localFileName": "${EMS_PUBLIC_DIR}/resources/baguette-client.tgz", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Upload installation package MD5 checksum", + "taskType": "COPY", + "fileName": "/tmp/baguette-client.tgz.md5", + "localFileName": "${EMS_PUBLIC_DIR}/resources/baguette-client.tgz.md5", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Check MD5 checksum of installation package", + "taskType": "CHECK", + "command": "[[ `cat /tmp/baguette-client.tgz.md5` != `md5sum /tmp/baguette-client.tgz | cut -d ' ' -f 1 ` ]] && exit 99", + "executable": false, + "exitCode": 99, + "match": true + }, + { + "description": "Extract installation package to target folder", + "taskType": "CMD", + "command": "${ROOT_CMD} tar zxvf /tmp/baguette-client.tgz -C ${BAGUETTE_CLIENT_BASE_DIR}/../ ", + "executable": false, + "exitCode": 0, + "match": false, + "executionTimeout": 120000 + }, + { + "description": "Change files and folders ownership", + "taskType": "CMD", + "command": "${ROOT_CMD} chown -R ${NODE_SSH_USERNAME} ${BAGUETTE_CLIENT_BASE_DIR}", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Touch files", + "taskType": "CMD", + "command": "touch ${BAGUETTE_CLIENT_BASE_DIR}/logs/output.txt", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Create conf directory", + "taskType": "CMD", + "command": "mkdir ${BAGUETTE_CLIENT_BASE_DIR}/conf/", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Copy-and-process configuration to target", + "taskType": "FILE", + "localFileName": "${EMS_CONFIG_DIR}/baguette-client/", + "fileName": "${BAGUETTE_CLIENT_BASE_DIR}", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Clean installation package from /tmp", + "taskType": "CMD", + "command": "rm -f /tmp/baguette-client.tgz*", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Write success file", + "taskType": "CMD", + "command": "echo SUCCESS >> ${BAGUETTE_CLIENT_BASE_DIR}/conf/ok.txt", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "-- LIST ${BAGUETTE_CLIENT_BASE_DIR}/.. AFTER --", + "taskType": "CMD", + "command": "ls -l ${BAGUETTE_CLIENT_BASE_DIR}/.. ", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "-- LIST baguette-client FILES --", + "taskType": "CMD", + "command": "ls -l ${BAGUETTE_CLIENT_BASE_DIR} ", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Set __EMS_CLIENT_INSTALL__ variable", + "taskType": "SET_VARS", + "variables": { + "__EMS_CLIENT_INSTALL__": "INSTALLED" + } + }, + { + "description": "Log installation end", + "taskType": "LOG", + "message": "EMS client installation completed at Node" + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/check-ignore.json b/ems-core/config-files/baguette-client-install/linux/check-ignore.json new file mode 100644 index 0000000..419f416 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/check-ignore.json @@ -0,0 +1,31 @@ +{ + "os": "LINUX", + "description": "Check if node must be ignored", + "condition": "! ${SKIP_IGNORE_CHECK:-false}", + "instructions": [ + { + "description": "Checking for .EMS_IGNORE_NODE file...", + "taskType": "LOG", + "message": "Checking for .EMS_IGNORE_NODE file..." + }, + { + "description": "Checking for .EMS_IGNORE_NODE file", + "taskType": "CHECK", + "command": "test -e /tmp/.EMS_IGNORE_NODE", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Set __EMS_IGNORE_NODE__ variable", + "taskType": "SET_VARS", + "variables": { + "__EMS_IGNORE_NODE__": "IGNORED" + } + }, + { + "description": "Stop further processing", + "taskType": "EXIT" + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/detect.json b/ems-core/config-files/baguette-client-install/linux/detect.json new file mode 100644 index 0000000..359537f --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/detect.json @@ -0,0 +1,69 @@ +{ + "os": "LINUX", + "description": "Detect node features (OS, architecture, cores, RAM, disk etc)", + "condition": "! ${SKIP_DETECTION:-false}", + "instructions": [ + { + "description": "Detecting target node type...", + "taskType": "LOG", + "message": "Detecting target node type..." + }, + { + "description": "Copying detection script to node...", + "taskType": "COPY", + "fileName": "/tmp/detect.sh", + "localFileName": "bin/detect.sh", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Make detection script executable", + "taskType": "CMD", + "command": "chmod +x /tmp/detect.sh ", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Run detection script", + "taskType": "CMD", + /*"command": "if [ ! -e /tmp/detect.txt ]; then /tmp/detect.sh &> /tmp/detect.txt; fi",*/ + "command": "/tmp/detect.sh &> /tmp/detect.txt", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Copying detection results back to EMS server...", + "taskType": "DOWNLOAD", + "fileName": "/tmp/detect.txt", + "localFileName": "logs/detect.${NODE_ADDRESS}--${TIMESTAMP-FILE}.txt", + "executable": false, + "exitCode": 0, + "match": false, + "patterns": { + "CPU_SOCKETS": "^\\s*CPU_SOCKETS\\s*[=:]\\s*(.*)\\s*", + "CPU_CORES": "^\\s*CPU_CORES\\s*[=:]\\s*(.*)\\s*", + "CPU_PROCESSORS": "^\\s*CPU_PROCESSORS\\s*[=:]\\s*(.*)\\s*", + "RAM_TOTAL_KB": "^\\s*RAM_TOTAL_KB\\s*[=:]\\s*(.*)\\s*", + "RAM_AVAILABLE_KB": "^\\s*RAM_AVAILABLE_KB\\s*[=:]\\s*(.*)\\s*", + "RAM_FREE_KB": "^\\s*RAM_FREE_KB\\s*[=:]\\s*(.*)\\s*", + "RAM_USED_KB": "^\\s*RAM_USED_KB\\s*[=:]\\s*(.*)\\s*", + "RAM_UTILIZATION": "^\\s*RAM_UTILIZATION\\s*[=:]\\s*(.*)\\s*", + "DISK_TOTAL_KB": "^\\s*DISK_TOTAL_KB\\s*[=:]\\s*(.*)\\s*", + "DISK_FREE_KB": "^\\s*DISK_FREE_KB\\s*[=:]\\s*(.*)\\s*", + "DISK_USED_KB": "^\\s*DISK_USED_KB\\s*[=:]\\s*(.*)\\s*", + "DISK_UTILIZATION": "^\\s*DISK_UTILIZATION\\s*[=:]\\s*(.*)\\s*", + "OS_ARCHITECTURE": "^\\s*OS_ARCHITECTURE\\s*[=:]\\s*(.*)\\s*", + "OS_KERNEL": "^\\s*OS_KERNEL\\s*[=:]\\s*(.*)\\s*", + "OS_KERNEL_RELEASE": "^\\s*OS_KERNEL_RELEASE\\s*[=:]\\s*(.*)\\s*" + } + }, + { + "description": "Detection results...", + "taskType": "LOG", + "message": "Detection results:\n CPU_SOCKETS=${CPU_SOCKETS:-na}\n CPU_CORES=${CPU_CORES:-na}\n CPU_PROCESSORS=${CPU_PROCESSORS:-na}\n RAM_TOTAL_KB=${RAM_TOTAL_KB:-na}\n RAM_AVAILABLE_KB=${RAM_AVAILABLE_KB:-na}\n RAM_FREE_KB=${RAM_FREE_KB:-na}\n RAM_USED_KB=${RAM_USED_KB:-na}\n RAM_UTILIZATION=${RAM_UTILIZATION:-na}\n DISK_TOTAL_KB=${DISK_TOTAL_KB:-na}\n DISK_FREE_KB=${DISK_FREE_KB:-na}\n DISK_USED_KB=${DISK_USED_KB:-na}\n DISK_UTILIZATION=${DISK_UTILIZATION:-na}\n OS_ARCHITECTURE=${OS_ARCHITECTURE:-na}\n OS_KERNEL=${OS_KERNEL:-na}\n OS_KERNEL_RELEASE=${OS_KERNEL_RELEASE:-na}" + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/jre.json b/ems-core/config-files/baguette-client-install/linux/jre.json new file mode 100644 index 0000000..0f8259f --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/jre.json @@ -0,0 +1,79 @@ +{ + "os": "LINUX", + "description": "JRE installation instruction set at VM node", + "condition": "! ${SKIP_JRE_INSTALLATION:-false} && ! '${OS_ARCHITECTURE:-x}'.startsWith('arm') && ${CPU_PROCESSORS:-0} > ${BAGUETTE_INSTALLATION_MIN_PROCESSORS:-0} && ${RAM_AVAILABLE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_RAM:-0} && ${DISK_FREE_KB:-0} > ${BAGUETTE_INSTALLATION_MIN_DISK_FREE:-0}", + "instructions": [ + { + "description": "Check if JRE is already installed at Node", + "taskType": "CHECK", + "command": "[[ -f ${BAGUETTE_CLIENT_BASE_DIR}/jre/bin/java ]] && exit 99", + "executable": false, + "exitCode": 99, + "match": true, + "message": "====== JRE is already installed at Node ======" + }, + { + "description": "Install JRE...", + "taskType": "LOG", + "message": "Install JRE..." + }, + { + "description": "Mkdir Baguette Client installation folder", + "taskType": "CMD", + "command": "${ROOT_CMD} mkdir -p ${BAGUETTE_CLIENT_BASE_DIR} ", + "executable": false, + "exitCode": 0, + "match": false, + "executionTimeout": 120000 + }, + /*{ + "description": "Download JRE package", + "taskType": "CMD", + "command": "curl -k ${DOWNLOAD_URL}/resources/zulu8.52.0.23-ca-jre8.0.282-linux_x64.tar.gz --output /tmp/jre8.282.tar.gz", + "executable": false, + "exitCode": 0, + "match": false + },*/ + { + "description": "Copy JRE package", + "taskType": "COPY", + "fileName": "/tmp/${JRE_LINUX_PACKAGE}", + "localFileName": "${EMS_PUBLIC_DIR}/resources/${JRE_LINUX_PACKAGE}", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Extract JRE package into installation folder", + "taskType": "CMD", + "command": "${ROOT_CMD} tar zxvf /tmp/${JRE_LINUX_PACKAGE} -C ${BAGUETTE_CLIENT_BASE_DIR}", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Rename JRE directory", + "taskType": "CMD", + "command": "${ROOT_CMD} mv ${BAGUETTE_CLIENT_BASE_DIR}/zulu* ${BAGUETTE_CLIENT_BASE_DIR}/jre", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "List BC home directory", + "taskType": "CMD", + "command": "ls -l ${BAGUETTE_CLIENT_BASE_DIR}", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Print JRE version", + "taskType": "CMD", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/jre/bin/java -version", + "executable": false, + "exitCode": 0, + "match": false + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/netdata.json b/ems-core/config-files/baguette-client-install/linux/netdata.json new file mode 100644 index 0000000..2476dea --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/netdata.json @@ -0,0 +1,57 @@ +{ + "os": "LINUX", + "description": "Netdata installation instruction set at VM node", + "condition": "! ${SKIP_NETDATA_INSTALLATION:-false}", + "instructions": [ + { + "description": "Log Netdata installation start", + "taskType": "LOG", + "message": "Starting Netdata installation at Node" + }, + { + "description": "Check if Netdata is already installed at Node", + "taskType": "CHECK", + "command": "[[ -f /usr/sbin/netdata ]] && exit 99", + /*"command": "[ $(ps -e -o pid,comm,cgroup |grep netdata |grep -v docker |grep -v lxc |wc -l) -gt 0 ] && exit 99",*/ + "executable": false, + "exitCode": 99, + "match": true, + "message": "====== Netdata is already installed at Node ======" + }, + { + "description": "Log Wait if apt is being updated", + "taskType": "LOG", + "message": "Wait if apt is being updated" + }, + { + "description": "Start unattended-upgrade if available", + "taskType": "CMD", + "command": "if command -v unattended-upgrade &> /dev/null ; then unattended-upgrade -d ; else echo \"Command 'unattended-upgrade' is not available\" ; fi", + "executionTimeout": 600000 + }, + { + "description": "Wait if apt is being updated", + "taskType": "CMD", + "command": "while [ `ps aux | grep -i lock_is_held | grep -v grep | wc -l` != 0 ]; do echo \"Lock_is_held...\"; ps aux | grep -i lock_is_held ; sleep 10 ; done", + "executionTimeout": 600000 + }, + { + "description": "Download Netdata kickstart.sh", + "taskType": "CMD", + "command": "curl https://my-netdata.io/kickstart-static64.sh > /tmp/netdata-kickstart.sh", + "executionTimeout": 600000 + }, + { + "description": "Make Netdata kickstart.sh executable", + "taskType": "CMD", + "command": "chmod +x /tmp/netdata-kickstart.sh", + "executionTimeout": 600000 + }, + { + "description": "Run Netdata kickstart.sh", + "taskType": "CMD", + "command": "echo ${NODE_SSH_PASSWORD} | sudo -S sh /tmp/netdata-kickstart.sh --dont-wait --no-updates --disable-telemetry --dont-start-it --stable-channel --disable-cloud ", + "executionTimeout": 600000 + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/recover-baguette.json b/ems-core/config-files/baguette-client-install/linux/recover-baguette.json new file mode 100644 index 0000000..846683b --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/recover-baguette.json @@ -0,0 +1,24 @@ +{ + "os": "LINUX", + "description": "Restarting Baguette agent at VM node", + "instructions": [ + { + "description": "Killing previous EMS client process", + "taskType": "CMD", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/kill.sh", + "executable": false, + "exitCode": 0, + "match": false, + "retries": 5 + }, + { + "description": "Starting new EMS client process", + "taskType": "CMD", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/run.sh", + "executable": false, + "exitCode": 0, + "match": false, + "retries": 5 + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/linux/start-agents.json b/ems-core/config-files/baguette-client-install/linux/start-agents.json new file mode 100644 index 0000000..1307a2a --- /dev/null +++ b/ems-core/config-files/baguette-client-install/linux/start-agents.json @@ -0,0 +1,52 @@ +{ + "os": "LINUX", + "description": "Starting Netdata and Baguette agents at VM node", + "condition": "! ${SKIP_START:-false}", + "instructions": [ + { + "description": "Launch EMS client", + "taskType": "CMD", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/run.sh", + "executable": false, + "exitCode": 0, + "match": false, + "retries": 5 + }, + { + "description": "Check if Netdata is already running", + "taskType": "CHECK", + /*"command": "[[ $(( `ps -ef |grep /usr/sbin/netdata |grep -v grep |wc -l`+1 )) -gt 1 ]] && exit 1 || exit 0",*/ + "command": "[[ $(ps -e -o pid,comm,cgroup |grep netdata |grep -v grep |grep -v docker |grep -v lxc |wc -l) -gt 0 ]] && exit 1 || exit 0", + "executable": false, + "exitCode": 1, + "match": true, + "message": "Netdata is already running" + }, + { + "description": "Copy Netdata Prometheus plugin configuration to node's /tmp directory", + "taskType": "FILE", + "localFileName": "${EMS_CONFIG_DIR}/baguette-client-install/netdata/go.d/prometheus.conf", + "fileName": "/tmp", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Move prometheus config from /tmp to /etc/netdata/go.d/ directory", + "taskType": "CMD", + "command": "echo ${NODE_SSH_PASSWORD} | sudo -- sh -c 'mkdir -p /etc/netdata/go.d/ && mv -f /tmp/prometheus.conf /etc/netdata/go.d/' ", + "executable": false, + "exitCode": 0, + "match": false + }, + { + "description": "Launch Netdata", + "taskType": "CMD", + "command": "echo ${NODE_SSH_PASSWORD} | sudo -S -- sh -c '/opt/netdata/bin/netdata || /usr/sbin/netdata || netdata' ", + "executable": false, + "exitCode": 0, + "match": false, + "retries": 5 + } + ] +} \ No newline at end of file diff --git a/ems-core/config-files/baguette-client-install/netdata/go.d/prometheus.conf b/ems-core/config-files/baguette-client-install/netdata/go.d/prometheus.conf new file mode 100644 index 0000000..a096044 --- /dev/null +++ b/ems-core/config-files/baguette-client-install/netdata/go.d/prometheus.conf @@ -0,0 +1,4 @@ +# Based on: https://raw.githubusercontent.com/netdata/go.d.plugin/master/config/go.d/prometheus.conf +# Netdata go.d plugin configuration for prometheus + +${NETDATA_PROMETHEUS_CONF} diff --git a/ems-core/config-files/baguette-client-install/win/win.json b/ems-core/config-files/baguette-client-install/win/win.json new file mode 100644 index 0000000..e69de29 diff --git a/ems-core/config-files/baguette-client/conf/baguette-client.properties.sample b/ems-core/config-files/baguette-client/conf/baguette-client.properties.sample new file mode 100644 index 0000000..b493650 --- /dev/null +++ b/ems-core/config-files/baguette-client/conf/baguette-client.properties.sample @@ -0,0 +1,224 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +################################################################################ +### EMS - Baguette Client properties ### +################################################################################ + +#password-encoder-class = password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder +#password-encoder-class = password.gr.iccs.imu.ems.util.IdentityPasswordEncoder +#password-encoder-class = password.gr.iccs.imu.ems.util.PresentPasswordEncoder + +### Jasypt encryptor settings (using old settings until encrypted texts are updated) +jasypt.encryptor.algorithm = PBEWithMD5AndDES +jasypt.encryptor.ivGeneratorClassname = org.jasypt.iv.NoIvGenerator + +# Baguette Client configuration + +baseDir = ${BAGUETTE_CLIENT_BASE_DIR} +connection-retry-enabled = true +connection-retry-delay = 10000 +connection-retry-limit = -1 +auth-timeout = 60000 +exec-timeout = 120000 +#retry-period = 60000 +exit-command-allowed = false +#kill-delay = 10 + +IP_SETTING=${IP_SETTING} +EMS_CLIENT_ADDRESS=${${IP_SETTING}} + +node-properties= + +# ----------------------------------------------------------------------------- +# Client Id and Baguette Server credentials +# ----------------------------------------------------------------------------- + +client-id = ${BAGUETTE_CLIENT_ID} + +#server-address = ${BAGUETTE_SERVER_HOSTNAME} +server-address = ${BAGUETTE_SERVER_ADDRESS} +server-port = ${BAGUETTE_SERVER_PORT} +server-pubkey = ${BAGUETTE_SERVER_PUBKEY} +server-pubkey-fingerprint = ${BAGUETTE_SERVER_PUBKEY_FINGERPRINT} +server-pubkey-algorithm = ${BAGUETTE_SERVER_PUBKEY_ALGORITHM} +server-pubkey-format = ${BAGUETTE_SERVER_PUBKEY_FORMAT} + +server-username = ${BAGUETTE_SERVER_USERNAME} +server-password = ${BAGUETTE_SERVER_PASSWORD} + +# ----------------------------------------------------------------------------- +# Client-side Self-healing settings +# ----------------------------------------------------------------------------- + +#self.healing.enabled=true +#self.healing.recovery.file.baguette=conf/baguette.json +#self.healing.recovery.file.netdata=conf/netdata.json +#self.healing.recovery.delay=10000 +#self.healing.recovery.retry.wait=60000 +#self.healing.recovery.max.retries=3 + +# ----------------------------------------------------------------------------- +# Collectors settings +# ----------------------------------------------------------------------------- + +#collector-classes = netdata.collector.gr.iccs.imu.ems.baguette.client.NetdataCollector + +collector.netdata.enable = true +collector.netdata.delay = 10000 +collector.netdata.url = http://127.0.0.1:19999/api/v1/allmetrics?format=json +collector.netdata.urlOfNodesWithoutClient = http://%s:19999/api/v1/allmetrics?format=json +#collector.netdata.create-topic = true +#collector.netdata.allowed-topics = netdata__system__cpu__user:an_alias +collector.netdata.allowed-topics = ${COLLECTOR_ALLOWED_TOPICS} +collector.netdata.error-limit = 3 +collector.netdata.pause-period = 60 + +collector.prometheus.enable = false +collector.prometheus.delay = 10000 +collector.prometheus.url = http://127.0.0.1:9090/metrics +collector.prometheus.urlOfNodesWithoutClient = http://%s:9090/metrics +#collector.prometheus.create-topic = true +#collector.prometheus.allowed-topics = system__cpu__user:an_alias +collector.prometheus.allowed-topics = ${COLLECTOR_ALLOWED_TOPICS} +collector.prometheus.error-limit = 3 +collector.prometheus.pause-period = 60 +# +#collector.prometheus.allowedTags = +#collector.prometheus.allowTagsInDestinationName = true +#collector.prometheus.destinationNameFormatter = ${metricName}_${method} +#collector.prometheus.addTagsAsEventProperties = true +#collector.prometheus.addTagsInEventPayload = true +#collector.prometheus.throwExceptionWhenExcessiveCharsOccur = true + +# ----------------------------------------------------------------------------- +# Cluster settings +# ----------------------------------------------------------------------------- + +#cluster.cluster-id=cluster +#cluster.local-node.id=local-node +#cluster.local-node.address=localhost:1234 +#cluster.local-node.properties.name=value +#cluster.member-addresses=[localhost:3456, localhost:5678] + +#cluster.useSwim=false +#cluster.failureTimeout=5000 +cluster.testInterval=5000 + +cluster.log-enabled=true +cluster.out-enabled=true + +cluster.join-on-init=true +cluster.election-on-join=false +#cluster.usePBInMg=true +#cluster.usePBInPg=true +#cluster.mgName=system +#cluster.pgName=data + +cluster.tls.enabled=true +#cluster.tls.keystore=${EMS_CONFIG_DIR}/cluster.jks +#cluster.tls.keystore-password=atomix +#cluster.tls.truststore=${EMS_CONFIG_DIR}/cluster.jks +#cluster.tls.truststore-password=atomix +cluster.tls.keystore-dir=conf + +cluster.score.formula=20*cpu/32+80*ram/(256*1024) +cluster.score.default-score=0 +cluster.score.default-args.cpu=1 +cluster.score.default-args.ram=128 +#cluster.score.throw-exception=false + + +################################################################################ +### EMS - Broker-CEP properties ### +################################################################################ + +# Broker ports and protocol +brokercep.broker-name = broker +brokercep.broker-port = 61617 +#brokercep.management-connector-port = 1088 +brokercep.broker-protocol = ssl +# Don't use in EMS server +#brokercep.bypass-local-broker = true + +# Common Broker settings +BROKER_URL_PROPERTIES = transport.daemon=true&transport.trace=false&transport.useKeepAlive=true&transport.useInactivityMonitor=false&transport.needClientAuth=${CLIENT_AUTH_REQUIRED}&transport.verifyHostName=true&transport.connectionTimeout=0&transport.keepAlive=true +CLIENT_AUTH_REQUIRED = false +brokercep.broker-url[0] = ${brokercep.broker-protocol}://0.0.0.0:${brokercep.broker-port}?${BROKER_URL_PROPERTIES} +brokercep.broker-url[1] = tcp://127.0.0.1:61616?${BROKER_URL_PROPERTIES} +brokercep.broker-url[2] = + +CLIENT_URL_PROPERTIES=daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true +brokercep.broker-url-for-consumer = tcp://127.0.0.1:61616?${CLIENT_URL_PROPERTIES} +brokercep.broker-url-for-clients = ${brokercep.broker-protocol}://${EMS_CLIENT_ADDRESS}:${brokercep.broker-port}?${CLIENT_URL_PROPERTIES} +# Must be a public IP address + +# Key store +brokercep.ssl.keystore-file = ${EMS_CONFIG_DIR}/client-broker-keystore.p12 +brokercep.ssl.keystore-type = PKCS12 +#brokercep.ssl.keystore-password = melodic +brokercep.ssl.keystore-password = ENC(ISMbn01HVPbtRPkqm2Lslg==) +# Trust store +brokercep.ssl.truststore-file = ${EMS_CONFIG_DIR}/client-broker-truststore.p12 +brokercep.ssl.truststore-type = PKCS12 +#brokercep.ssl.truststore-password = melodic +brokercep.ssl.truststore-password = ENC(ISMbn01HVPbtRPkqm2Lslg==) +# Certificate +brokercep.ssl.certificate-file = ${EMS_CONFIG_DIR}/client-broker.crt +# Key-and-Cert data +brokercep.ssl.key-entry-generate = IF-IP-CHANGED +brokercep.ssl.key-entry-name = ${EMS_CLIENT_ADDRESS} +brokercep.ssl.key-entry-dname = CN=${EMS_CLIENT_ADDRESS},OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR +brokercep.ssl.key-entry-ext-san = dns:localhost,ip:127.0.0.1,ip:${DEFAULT_IP},ip:${PUBLIC_IP} + +# Authentication and Authorization settings +brokercep.authentication-enabled = true +#brokercep.additional-broker-credentials = aaa/111, bbb/222, morphemic/morphemic +brokercep.additional-broker-credentials = ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ) +brokercep.authorization-enabled = false + +# Broker instance settings +brokercep.broker-persistence-enabled = false +brokercep.broker-using-jmx = true +brokercep.broker-advisory-support-enabled = true +brokercep.broker-using-shutdown-hook = false + +#brokercep.broker-enable-statistics = true +#brokercep.broker-populate-jmsx-user-id = true + +# Message interceptors +brokercep.message-interceptors[0].destination = > +brokercep.message-interceptors[0].className = interceptor.broker.gr.iccs.imu.ems.brokercep.SequentialCompositeInterceptor +brokercep.message-interceptors[0].params[0] = #SourceAddressMessageUpdateInterceptor +brokercep.message-interceptors[0].params[1] = #MessageForwarderInterceptor +brokercep.message-interceptors[0].params[2] = #NodePropertiesMessageUpdateInterceptor + +brokercep.message-interceptors-specs.SourceAddressMessageUpdateInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.SourceAddressMessageUpdateInterceptor +brokercep.message-interceptors-specs.MessageForwarderInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.MessageForwarderInterceptor +brokercep.message-interceptors-specs.NodePropertiesMessageUpdateInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.NodePropertiesMessageUpdateInterceptor + +# Message forward destinations (MessageForwarderInterceptor must be included in 'message-interceptors' property) +#brokercep.message-forward-destinations[0].connection-string = tcp://localhost:51515 +#brokercep.message-forward-destinations[0].username = AAA +#brokercep.message-forward-destinations[0].password = 111 +#brokercep.message-forward-destinations[1].connection-string = tcp://localhost:41414 +#brokercep.message-forward-destinations[1].username = AAA +#brokercep.message-forward-destinations[1].password = 111 + +# Advisory watcher +brokercep.enable-advisory-watcher = true + +# Memory usage limit +brokercep.usage.memory.jvm-heap-percentage = 20 +#brokercep.usage.memory.size = 134217728 + +#brokercep.maxEventForwardRetries: -1 +#brokercep.maxEventForwardDuration: -1 + +################################################################################ \ No newline at end of file diff --git a/ems-core/config-files/baguette-client/conf/baguette-client.yml b/ems-core/config-files/baguette-client/conf/baguette-client.yml new file mode 100644 index 0000000..d9d4237 --- /dev/null +++ b/ems-core/config-files/baguette-client/conf/baguette-client.yml @@ -0,0 +1,259 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +################################################################################ +### EMS - Baguette Client properties ### +################################################################################ + +#password-encoder-class: gr.iccs.imu.ems.util.password.AsterisksPasswordEncoder +#password-encoder-class: gr.iccs.imu.ems.util.password.IdentityPasswordEncoder +#password-encoder-class: gr.iccs.imu.ems.util.password.PresentPasswordEncoder + +### Jasypt encryptor settings (using old settings until encrypted texts are updated) +jasypt: + encryptor: + algorithm: PBEWithMD5AndDES + ivGeneratorClassname: org.jasypt.iv.NoIvGenerator + +# Baguette Client configuration + +baseDir: ${BAGUETTE_CLIENT_BASE_DIR} +connection-retry-enabled: true +connection-retry-delay: 10000 +connection-retry-limit: -1 +auth-timeout: 60000 +exec-timeout: 120000 +#retry-period: 60000 +exit-command-allowed: false +#kill-delay: 10 + +IP_SETTING: ${IP_SETTING} +EMS_CLIENT_ADDRESS: ${${IP_SETTING}} + +node-properties: + node-id: ${NODE_CLIENT_ID} + public-ip: ${NODE_ADDRESS} + private-ip: ${NODE_ADDRESS} + instance: ${NODE_ADDRESS} + host: ${NODE_ADDRESS} + zone: ${zone-id} + region: ${zone-id} + cloud: ${provider} + +# ----------------------------------------------------------------------------- +# Client Id and Baguette Server credentials +# ----------------------------------------------------------------------------- + +client-id: ${BAGUETTE_CLIENT_ID} + +#server-address: ${BAGUETTE_SERVER_HOSTNAME} +server-address: ${BAGUETTE_SERVER_ADDRESS} +server-port: ${BAGUETTE_SERVER_PORT} +server-pubkey: ${BAGUETTE_SERVER_PUBKEY} +server-pubkey-fingerprint: ${BAGUETTE_SERVER_PUBKEY_FINGERPRINT} +server-pubkey-algorithm: ${BAGUETTE_SERVER_PUBKEY_ALGORITHM} +server-pubkey-format: ${BAGUETTE_SERVER_PUBKEY_FORMAT} + +server-username: ${BAGUETTE_SERVER_USERNAME} +server-password: ${BAGUETTE_SERVER_PASSWORD} + +# ----------------------------------------------------------------------------- +# Client-side Self-healing settings +# ----------------------------------------------------------------------------- + +#self.healing: +# enabled: true +# recovery: +# file: +# baguette: conf/baguette.json +# netdata: conf/netdata.json +# delay: 10000 +# retry-delay: 60000 +# max-retries: 3 + +# ----------------------------------------------------------------------------- +# Collectors settings +# ----------------------------------------------------------------------------- + +#collector-classes: gr.iccs.imu.ems.baguette.client.collector.netdata.NetdataCollector + +collector: + netdata: + enable: true + delay: 10000 + url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + urlOfNodesWithoutClient: http://%s:19999/api/v1/allmetrics?format=json + #create-topic: true + #allowed-topics: netdata__system__cpu__user:an_alias + allowed-topics: ${COLLECTOR_ALLOWED_TOPICS} + error-limit: 3 + pause-period: 60 + prometheus: + enable: false + delay: 10000 + url: http://127.0.0.1:9090/metrics + urlOfNodesWithoutClient: http://%s:9090/metrics + #create-topic: true + #allowed-topics: system__cpu__user:an_alias + allowed-topics: ${COLLECTOR_ALLOWED_TOPICS} + error-limit: 3 + pause-period: 60 + # + #allowedTags: [] + #allowTagsInDestinationName: true + #destinationNameFormatter: '${metricName}_${method}' + #addTagsAsEventProperties: true + #addTagsInEventPayload: true + #throwExceptionWhenExcessiveCharsOccur: true + +# ----------------------------------------------------------------------------- +# Cluster settings +# ----------------------------------------------------------------------------- + +cluster: + #cluster-id: cluster + #local-node.id: local-node + #local-node.address: localhost:1234 + #local-node.properties: + # name: value + #member-addresses: [localhost:3456, localhost:5678] + + #useSwim: false + #failureTimeout: 5000 + testInterval: 5000 + + log-enabled: true + out-enabled: true + + join-on-init: true + election-on-join: false + #usePBInMg: true + #usePBInPg: true + #mgName: system + #pgName: data + + tls: + enabled: true + #keystore: ${EMS_CONFIG_DIR}/cluster.jks + #keystore-password: atomix + #truststore: ${EMS_CONFIG_DIR}/cluster.jks + #truststore-password: atomix + keystore-dir: conf + + score: + formula: 20*cpu/32+80*ram/(256*1024) + default-score: 0 + default-args: + cpu: 1 + ram: 128 + #throw-exception: false + + +################################################################################ +### EMS - Broker-CEP properties ### +################################################################################ + +BROKER_URL_PROPERTIES: transport.daemon=true&transport.trace=false&transport.useKeepAlive=true&transport.useInactivityMonitor=false&transport.needClientAuth=${CLIENT_AUTH_REQUIRED}&transport.verifyHostName=true&transport.connectionTimeout=0&transport.keepAlive=true +CLIENT_AUTH_REQUIRED: false +CLIENT_URL_PROPERTIES: daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true + +brokercep: + # Broker ports and protocol + broker-name: broker + broker-port: 61617 + broker-protocol: ssl + #management-connector-port: 1088 + #bypass-local-broker: true # Don't use in EMS server + + # Broker connectors + broker-url: + - ${brokercep.broker-protocol}://0.0.0.0:${brokercep.broker-port}?${BROKER_URL_PROPERTIES} + - tcp://127.0.0.1:61616?${BROKER_URL_PROPERTIES} + - stomp://127.0.0.1:61610?${BROKER_URL_PROPERTIES} + + # Broker URLs for (EMS) consumer and clients + broker-url-for-consumer: tcp://127.0.0.1:61616?${CLIENT_URL_PROPERTIES} + broker-url-for-clients: ${brokercep.broker-protocol}://${EMS_CLIENT_ADDRESS}:${brokercep.broker-port}?${CLIENT_URL_PROPERTIES} + # Must be a public IP address + + ssl: + # Key store settings + keystore-file: ${EMS_CONFIG_DIR}/client-broker-keystore.p12 + keystore-type: PKCS12 + keystore-password: 'ENC(ISMbn01HVPbtRPkqm2Lslg==)' # melodic + + # Trust store settings + truststore-file: ${EMS_CONFIG_DIR}/client-broker-truststore.p12 + truststore-type: PKCS12 + truststore-password: 'ENC(ISMbn01HVPbtRPkqm2Lslg==)' # melodic + + # Certificate settings + certificate-file: ${EMS_CONFIG_DIR}/client-broker.crt + + # key generation settings + key-entry-generate: IF-IP-CHANGED + key-entry-name: ${EMS_CLIENT_ADDRESS} + key-entry-dname: 'CN=${EMS_CLIENT_ADDRESS},OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR' + key-entry-ext-san: 'dns:localhost,ip:127.0.0.1,ip:${DEFAULT_IP},ip:${PUBLIC_IP}' + + # Authentication and Authorization settings + authentication-enabled: true + #additional-broker-credentials: aaa/111, bbb/222, morphemic/morphemic + additional-broker-credentials: 'ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ)' + authorization-enabled: false + + # Broker instance settings + broker-persistence-enabled: false + broker-using-jmx: true + broker-advisory-support-enabled: true + broker-using-shutdown-hook: false + + #broker-enable-statistics: true + #broker-populate-jmsx-user-id: true + + # Message interceptors + message-interceptors: + - destination: '>' + className: 'gr.iccs.imu.ems.brokercep.broker.interceptor.SequentialCompositeInterceptor' + params: + - '#SourceAddressMessageUpdateInterceptor' + - '#MessageForwarderInterceptor' + - '#NodePropertiesMessageUpdateInterceptor' + + message-interceptors-specs: + SourceAddressMessageUpdateInterceptor: + className: gr.iccs.imu.ems.brokercep.broker.interceptor.SourceAddressMessageUpdateInterceptor + MessageForwarderInterceptor: + className: gr.iccs.imu.ems.brokercep.broker.interceptor.MessageForwarderInterceptor + NodePropertiesMessageUpdateInterceptor: + className: gr.iccs.imu.ems.brokercep.broker.interceptor.NodePropertiesMessageUpdateInterceptor + + # Message forward destinations (MessageForwarderInterceptor must be included in 'message-interceptors' property) + #message-forward-destinations: + # - connection-string: tcp://localhost:51515 + # username: AAA + # password: 111 + # - connection-string: tcp://localhost:41414 + # username: AAA + # password: 111 + + # Advisory watcher + enable-advisory-watcher: true + + # Memory usage limit + usage: + memory: + jvm-heap-percentage: 20 + #size: 134217728 + + # Event forward settings + #maxEventForwardRetries: -1 + #maxEventForwardDuration: -1 + +################################################################################ \ No newline at end of file diff --git a/ems-core/config-files/baguette-client/conf/baguette.json b/ems-core/config-files/baguette-client/conf/baguette.json new file mode 100644 index 0000000..0e95dd2 --- /dev/null +++ b/ems-core/config-files/baguette-client/conf/baguette.json @@ -0,0 +1,16 @@ +[{ + "name": "Initial wait...", + "command": "pwd", + "waitBefore": 0, + "waitAfter": 5000 +}, { + "name": "Sending baguette client kill command...", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/kill.sh", + "waitBefore": 0, + "waitAfter": 2000 +}, { + "name": "Sending baguette client start command...", + "command": "${BAGUETTE_CLIENT_BASE_DIR}/bin/run.sh", + "waitBefore": 0, + "waitAfter": 10000 +}] diff --git a/ems-core/config-files/baguette-client/conf/logback-spring.xml b/ems-core/config-files/baguette-client/conf/logback-spring.xml new file mode 100644 index 0000000..e539f48 --- /dev/null +++ b/ems-core/config-files/baguette-client/conf/logback-spring.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %msg%n + + + + + + + + + + + + + + + + + + + + diff --git a/ems-core/config-files/baguette-client/conf/netdata.json b/ems-core/config-files/baguette-client/conf/netdata.json new file mode 100644 index 0000000..ed40f82 --- /dev/null +++ b/ems-core/config-files/baguette-client/conf/netdata.json @@ -0,0 +1,16 @@ +[{ + "name": "Initial wait...", + "command": "pwd", + "waitBefore": 0, + "waitAfter": 5000 +}, { + "name": "Sending Netdata agent kill command...", + "command": "sudo sh -c 'ps -U netdata -o \"pid\" --no-headers | xargs kill -9' ", + "waitBefore": 0, + "waitAfter": 2000 +}, { + "name": "Sending Netdata agent start command...", + "command": "sudo netdata", + "waitBefore": 0, + "waitAfter": 10000 +}] diff --git a/ems-core/config-files/ems-server.properties.sample b/ems-core/config-files/ems-server.properties.sample new file mode 100644 index 0000000..2a4c586 --- /dev/null +++ b/ems-core/config-files/ems-server.properties.sample @@ -0,0 +1,608 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +################################################################################ +### Global settings +################################################################################ + +### Don't touch the next line!! +EMS_SERVER_ADDRESS=${${control.IP_SETTING}} +DOLLAR=$ + +### Password Encoder settings +#password-encoder-class = password.gr.iccs.imu.ems.util.AsterisksPasswordEncoder +#password-encoder-class = password.gr.iccs.imu.ems.util.IdentityPasswordEncoder +#password-encoder-class = password.gr.iccs.imu.ems.util.PresentPasswordEncoder + +### Jasypt encryptor settings (using old settings until encrypted texts are updated) +jasypt.encryptor.algorithm = PBEWithMD5AndDES +jasypt.encryptor.ivGeneratorClassname = org.jasypt.iv.NoIvGenerator + +### Execution (@EnableAsync) and Scheduling (@EnableScheduling) thread pools +#spring.task.execution.pool.max-size = 16 +#spring.task.execution.pool.queue-capacity = 100 +#spring.task.execution.pool.keep-alive = 10s +#spring.task.scheduling.pool.size = 2 + +### Misc +spring.output.ansi.enabled=ALWAYS +spring.jackson.default-property-inclusion=non_null + + +################################################################################ +### Web server port and TLS settings +################################################################################ + +server.port = 8111 + +server.ssl.enabled=true + +### Keystore/Truststore settings +server.ssl.key-store=${control.ssl.keystore-file} +server.ssl.key-store-password=${control.ssl.keystore-password} +server.ssl.key-store-type=${control.ssl.keystore-type} +server.ssl.key-alias=${control.ssl.key-entry-name} +#server.ssl.key-password=${control.ssl.key-entry-password} + +### SSL ciphers and protocol settings +# SSL ciphers +#server.ssl.ciphers=TLS_RSA_WITH_AES_128_CBC_SHA256 +# SSL protocol to use +#server.ssl.protocol=TLS +# Enabled SSL protocols +#server.ssl.enabled-protocols=TLSv1.2 + +#security.require-ssl=true + + +################################################################################ +### JWT settings +jwt.secret=ENC(I0mRWgH2FVDDNs4OBcdh7Z+o3lOQDa3ztaEtmnXT2HN0aClkChp/lqm9zM5HyTk0stJ7v2Di75U=) +#jwt.expirationTime=86400000 +#jwt.refreshTokenExpirationTime=86400000 + + +################################################################################ +### Authorization settings +### NOTE: More authorization settings in 'authorization-client.properties' + +authorization.enabled = false +#authorization.paths-protected = /camelModel*, /cpModel*, /ems/**, /baguette/**, /event/**, /monitors +#authorization.paths-excluded = + + +################################################################################ +### Logback configuration file +logging.config=file:${EMS_CONFIG_DIR}/logback-conf/logback-spring.xml + + +################################################################################ +### Web Log-viewer configuration +log-viewer.url-mapping=/log-viewer + + +################################################################################ +### EMS - Control Service properties ### +################################################################################ + +### Don't touch the next lines!! +control.IP_SETTING=${EMS_IP_SETTING:PUBLIC_IP} +#control.EXECUTIONWARE=CLOUDIATOR +control.EXECUTIONWARE=PROACTIVE + +### URLs of Upperware services being invoked by EMS +control.esb-url = ${ESB_URL:https://mule:8088} +control.metasolver-configuration-url = ${METASOLVER_URL:http://metasolver:8092/updateConfiguration} + +### Log settings +#control.print-build-info=true +control.log-requests = ${EMS_LOG_REQUESTS:false} + +### Debug settings - Deactivate processing modules +#control.skip-translation = true +#control.skip-mvv-retrieve = true +#control.skip-broker-cep = true +#control.skip-baguette = true +#control.skip-collectors = true +#control.skip-metasolver = true +#control.skip-esb-notification = true +control.upperware-grouping = GLOBAL + +### Debug settings - Load/Save translation results +control.tc-load-file = ${EMS_TC_LOAD_FILE:${EMS_TC_FILE:${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/_TC.json}} +control.tc-save-file = ${EMS_TC_SAVE_FILE:${EMS_TC_FILE:${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/_TC.json}} + +### Process CAMEL and CP models on start-up +### Process CAMEL model on start-up +control.preload.camel-model = ${EMS_PRELOAD_CAMEL_MODEL:} + +### Use CP model on start-up +control.preload.cp-model = ${EMS_PRELOAD_CP_MODEL:} + +### Exit settings +control.exit-allowed = false +control.exit-grace-period = 10 +control.exit-code = 0 + +### Key store, Trust store and Certificate settings + +# Key store settings +control.ssl.keystore-file = ${EMS_CONFIG_DIR}/ems-keystore.p12 +control.ssl.keystore-type = PKCS12 +#control.ssl.keystore-password = melodic +#control.ssl.keystore-password = ENC(ISMbn01HVPbtRPkqm2Lslg==) + +# Trust store settings +control.ssl.truststore-file = ${EMS_CONFIG_DIR}/ems-truststore.p12 +control.ssl.truststore-type = PKCS12 +#control.ssl.truststore-password = melodic +#control.ssl.truststore-password = ENC(ISMbn01HVPbtRPkqm2Lslg==) + +# Certificate settings +control.ssl.certificate-file = ${EMS_CONFIG_DIR}/ems-cert.crt + +# EMS key generation settings +control.ssl.key-entry-generate = ALWAYS +control.ssl.key-entry-name = ems +#control.ssl.key-entry-password = +control.ssl.key-entry-dname = CN=ems,OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR +control.ssl.key-entry-ext-san = dns:localhost,ip:127.0.0.1,ip:${DEFAULT_IP},ip:${PUBLIC_IP} + + +################################################################################ +### Web configuration - Static resources + +### Static Web Resources and Redirects + +### Favicon settings +#web.static.favicon-context=/favicon.ico +web.static.favicon-path=file:${PUBLIC_DIR}/favicon.ico + +### Static resource settings +web.static.resource-context=/** +web.static.resource-path=file:${PUBLIC_DIR}/ + +web.static.logs-context=/logs/** +web.static.logs-path=file:${LOGS_DIR}/ + +### Redirects +#web.static.redirect=/resources/index.html +web.static.redirects.[/]=/admin/index.html +web.static.redirects.[/index.html]=/admin/index.html +web.static.redirects.[/admin]=/admin/index.html +web.static.redirects.[/admin/]=/admin/index.html +web.static.redirects.[/resources]=/resources/index.html +web.static.redirects.[/resources/]=/resources/index.html + + +################################################################################ +### Web & REST Security configuration + +### NOTE: Setting this to 'false' will turn off all security features +#melodic.security.enabled=false + +### JWT authentication ### +#web.security.jwt-authentication.enabled=false +#web.security.jwt-authentication.request-parameter=jwt +#web.security.jwt-authentication.print-sample-token=false + +### API Key access ### +#web.security.api-key-authentication.enabled=false +#web.security.api-key-authentication.value=${random.uuid} +#web.security.api-key-authentication.value=1234567890 +#web.security.api-key-authentication.request-header=EMS-API-KEY +#web.security.api-key-authentication.request-parameter=ems-api-key + +### OTP access ### +#web.security.otp-authentication.enabled=false +#web.security.otp-authentication.duration=3600000 +#web.security.otp-authentication.request-header=EMS-OTP +#web.security.otp-authentication.request-parameter=ems-otp + +### User Web Form authentication ### +#web.security.form-authentication.enabled=false +#web.security.form-authentication.username=admin +#web.security.form-authentication.password=ems + + +################################################################################ +### Topic Beacon settings + +beacon.enabled = true +beacon.initial-delay = 60000 +beacon.delay = 60000 +#beacon.rate = 60000 +#use-delay = false +beacon.heartbeat-topics = +beacon.threshold-topics = _ui_threshold_info +beacon.instance-topics = _ui_instance_info +beacon.prediction-topics = metrics_to_predict +beacon.prediction-rate = 60000 +beacon.slo-violation-detector-topics = metric.metric_list + + +################################################################################ +### Info Service settings + +info.metrics-update-interval=1000 +info.metrics-client-update-interval=10000 +# 'info.metrics-stream' value in seconds +info.metrics-stream-update-interval=10 +info.metrics-stream-event-name=ems-metrics-event +info.env-var-prefixes[0]=WEBSSH_SERVICE_-^ +info.env-var-prefixes[1]=WEB_ADMIN_!^ +# ! at the end means to trim off the prefix; - at the end means to convert '_' to '-'; +# ^ at the end means convert to upper case; ~ at the end means convert to lower case; + +################################################################################ +### Collectors settings + +collector.netdata.enable = true +collector.netdata.delay = 10000 +collector.netdata.skipLocal = true +collector.netdata.url = http://127.0.0.1:19999/api/v1/allmetrics?format=json +collector.netdata.urlOfNodesWithoutClient = http://%s:19999/api/v1/allmetrics?format=json +#collector.netdata.create-topic = true +#collector.netdata.allowed-topics = netdata__system__cpu__user:an_alias +collector.netdata.error-limit = 3 +collector.netdata.pause-period = 60 + + +################################################################################ +### Management and Endpoint settings + +management.info.build.enabled=true +management.info.env.enabled=true +management.info.git.enabled=true +management.info.java.enabled=true +management.endpoints.web.exposure.include=health,info +#management.endpoints.web.exposure.include=health,info,hawtio,jolokia +#management.endpoints.web.base-path=/ +#management.endpoint.health.show-details=always +#management.security.enabled=false +#management.port=9001 +#management.address=127.0.0.1 +#endpoints.metrics.sensitive=false + +### Hawtio web console settings +#management.endpoints.web.path-mapping.hawtio=hawtio/console +# NOTE: Uncomment to enable actuator and hawtio +#hawtio.authenticationEnabled=false +#hawtio.proxyWhitelist= +#hawtio.realm=hawtio +#hawtio.role=admin,viewer +#hawtio.rolePrincipalClasses=org.apache.activemq.jaas.GroupPrincipal + +### Jolokia (HTTP-JMX bridge) settings +#jolokia.config.debug=false +#endpoints.jolokia.enabled=true +#endpoints.jolokia.sensitive = false +#endpoints.jolokia.path=/jolokia +#spring.jmx.enabled=true +#endpoints.jmx.enabled=true + +################################################################################ +### Spring Boot Admin Client settings +#spring.boot.admin.client.url=http://localhost:8080 +#spring.boot.admin.client.username=username +#spring.boot.admin.client.password=password +#spring.boot.admin.client.instance.service-base-url=http://localhost:8080 + + +################################################################################ +### EMS - Broker-CEP properties ### +################################################################################ + +BROKER_URL_PROPERTIES = transport.daemon=true&transport.trace=false&transport.useKeepAlive=true&transport.useInactivityMonitor=false&transport.needClientAuth=${CLIENT_AUTH_REQUIRED}&transport.verifyHostName=true&transport.connectionTimeout=0&transport.keepAlive=true +CLIENT_AUTH_REQUIRED = false +CLIENT_URL_PROPERTIES=daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true + +# Broker name, ports and protocol +#brokercep.broker-name = broker +brokercep.broker-port = 61617 +brokercep.broker-protocol = ssl +#brokercep.management-connector-port = 1099 +# Don't use in EMS server +#brokercep.bypass-local-broker = true + +# Broker connectors +brokercep.broker-url[0] = ${brokercep.broker-protocol}://0.0.0.0:${brokercep.broker-port}?${BROKER_URL_PROPERTIES} +brokercep.broker-url[1] = tcp://0.0.0.0:61616?${BROKER_URL_PROPERTIES} +brokercep.broker-url[2] = stomp://0.0.0.0:61610 + +# Broker URLs for (EMS) consumer and clients +brokercep.broker-url-for-consumer = tcp://${EMS_SERVER_ADDRESS}:61616?${CLIENT_URL_PROPERTIES} +brokercep.broker-url-for-clients = ${brokercep.broker-protocol}://${EMS_SERVER_ADDRESS}:${brokercep.broker-port}?${CLIENT_URL_PROPERTIES} +# Must be a public IP address + +# Key store settings +brokercep.ssl.keystore-file=${EMS_CONFIG_DIR}/broker-keystore.p12 +brokercep.ssl.keystore-type=${control.ssl.keystore-type} +brokercep.ssl.keystore-password=${control.ssl.keystore-password} + +# Trust store settings +brokercep.ssl.truststore-file=${EMS_CONFIG_DIR}/broker-truststore.p12 +brokercep.ssl.truststore-type=${control.ssl.truststore-type} +brokercep.ssl.truststore-password=${control.ssl.truststore-password} + +# Certificate settings +brokercep.ssl.certificate-file=${EMS_CONFIG_DIR}/broker.crt + +# EMS key generation settings +brokercep.ssl.key-entry-generate=ALWAYS +brokercep.ssl.key-entry-name=${control.ssl.key-entry-name} +brokercep.ssl.key-entry-dname=${control.ssl.key-entry-dname} +brokercep.ssl.key-entry-ext-san=${control.ssl.key-entry-ext-san} + +# Authentication and Authorization settings +brokercep.authentication-enabled = true +#brokercep.additional-broker-credentials = aaa/111, bbb/222, morphemic/morphemic +#brokercep.additional-broker-credentials = ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ) +brokercep.authorization-enabled = false + +# Broker instance settings +brokercep.broker-persistence-enabled = false +brokercep.broker-using-jmx = true +brokercep.broker-advisory-support-enabled = true +brokercep.broker-using-shutdown-hook = false + +brokercep.broker-enable-statistics = true +brokercep.broker-populate-jmsx-user-id = true + +# Message interceptors +brokercep.message-interceptors[0].destination = > +brokercep.message-interceptors[0].className = interceptor.broker.gr.iccs.imu.ems.brokercep.SequentialCompositeInterceptor +brokercep.message-interceptors[0].params = #SourceAddressMessageUpdateInterceptor, #LogMessageUpdateInterceptor, #MessageForwarderInterceptor + +brokercep.message-interceptors-specs.SourceAddressMessageUpdateInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.SourceAddressMessageUpdateInterceptor +brokercep.message-interceptors-specs.LogMessageUpdateInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.LogMessageUpdateInterceptor +brokercep.message-interceptors-specs.MessageForwarderInterceptor.className = interceptor.broker.gr.iccs.imu.ems.brokercep.MessageForwarderInterceptor + +# Message forward destinations (MessageForwarderInterceptor must be included in 'message-interceptors' property) +#brokercep.message-forward-destinations[0].connection-string = tcp://localhost:51515 +#brokercep.message-forward-destinations[0].username = AAA +#brokercep.message-forward-destinations[0].password = 111 +#brokercep.message-forward-destinations[1].connection-string = tcp://localhost:41414 +#brokercep.message-forward-destinations[1].username = AAA +#brokercep.message-forward-destinations[1].password = 111 + +# Advisory watcher +brokercep.enable-advisory-watcher = true + +# Memory usage limit +brokercep.usage.memory.jvm-heap-percentage = 20 +#brokercep.usage.memory.size = 134217728 + +# Event forward settings +#brokercep.maxEventForwardRetries: -1 +#brokercep.maxEventForwardDuration: -1 + +# Event recorder settings +event-recorder.enabled=true +#event-recorder.format=JSON +event-recorder.file=${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/events-%T.%S +#event-recorder.filterMode: ALL | REGISTERED (default) | ALLOWED +#event-recorder.allowed-destinations: + + +################################################################################ +### EMS - Baguette Server properties ### +################################################################################ + +# Coordinator settings - Old style +baguette.server.coordinator-class = cluster.coordinator.gr.iccs.imu.ems.baguette.server.ClusteringCoordinator +#baguette.server.coordinatorParameters.param1 = p1 +#baguette.server.coordinatorParameters.param2 = p2 + +# Coordinator settings - New style +baguette.server.coordinator-id = clustering, 2level, noop +baguette.server.coordinatorConfig.clustering.coordinatorClass = cluster.coordinator.gr.iccs.imu.ems.baguette.server.ClusteringCoordinator +baguette.server.coordinatorConfig.clustering.parameters.zone-management-strategy-class = cluster.coordinator.gr.iccs.imu.ems.baguette.server.DefaultZoneManagementStrategy +baguette.server.coordinatorConfig.clustering.parameters.zone-port-start = 2000 +baguette.server.coordinatorConfig.clustering.parameters.zone-port-end = 2999 +baguette.server.coordinatorConfig.clustering.parameters.zone-keystore-file-name-formatter = ${LOGS_DIR:logs}/cluster_${DOLLAR}{TIMESTAMP}_${DOLLAR}{ZONE_ID}.p12 +#baguette.server.coordinatorConfig.clustering.parameters.cluster-detector-class = cluster.coordinator.gr.iccs.imu.ems.baguette.server.ClusterZoneDetector +#baguette.server.coordinatorConfig.clustering.parameters.cluster-detector-rules-type = MAP +#baguette.server.coordinatorConfig.clustering.parameters.cluster-detector-rules-separator = , +#baguette.server.coordinatorConfig.clustering.parameters.cluster-detector-rules = zone, zone-id, region, region-id, cloud, cloud-id, provider, provider-id +#baguette.server.coordinatorConfig.clustering.parameters.default-clusters = DEFAULT_CLUSTER_A, DEFAULT_CLUSTER_B +#baguette.server.coordinatorConfig.clustering.parameters.assignment-to-default-clusters = RANDOM +baguette.server.coordinatorConfig.2level.coordinatorClass = coordinator.gr.iccs.imu.ems.baguette.server.TwoLevelCoordinator +baguette.server.coordinatorConfig.noop.coordinatorClass = coordinator.gr.iccs.imu.ems.baguette.server.NoopCoordinator + +# Registration settings +#baguette.server.number-of-instances = 1 +baguette.server.registration-window = 30000 + +# SSH Server settings +baguette.server.address = ${EMS_SERVER_ADDRESS} +baguette.server.port = 2222 +baguette.server.key-file = ${EMS_CONFIG_DIR}/hostkey.pem +baguette.server.heartbeat-enabled = true +baguette.server.heartbeat-period = 60000 + +# SSH Server additional username/passwords +#baguette.server.credentials.aa=xx +#baguette.server.credentials.bb=yy + +# Client Id generation settings +#baguette.server.client-address-override-allowed=true +baguette.server.client-id-format-escape = ~ +baguette.server.client-id-format = ~{type:-_}-~{operatingSystem:-_}-~{id:-_}-~{name:-_}-~{provider:-_}-~{address:-_}-~{random:-_} + + +################################################################################ +### EMS - Baguette Client Install properties ### +################################################################################ + +### OS families +baguette.client.install.osFamilies.LINUX=CENTOS,DARWIN,DEBIAN,FEDORA ,FREEBSD ,GENTOO,COREOS,AMZN_LINUX,MANDRIVA ,NETBSD,OEL ,OPENBSD,RHEL,SCIENTIFIC,CEL,SLACKWARE,SOLARIS,SUSE,TURBOLINUX,CLOUD_LINUX,UBUNTU +baguette.client.install.osFamilies.WINDOWS=WINDOWS + +### Workers +baguette.client.install.workers=5 + +### Installation settings +### --- Root command --- +### E.g. 'echo ${NODE_SSH_PASSWORD} | sudo -S -- ' +baguette.client.install.rootCmd= + +### --- Directories and files --- +baguette.client.install.baseDir=~/baguette-client +baguette.client.install.mkdirs=${baguette.client.install.baseDir}/bin,${baguette.client.install.baseDir}/conf,${baguette.client.install.baseDir}/logs +baguette.client.install.touchFiles=${baguette.client.install.baseDir}/logs/output.txt +baguette.client.install.checkInstalledFile=${baguette.client.install.baseDir}/conf/ok.txt + +### --- Installation script URL and file (obsolete) --- +baguette.client.install.downloadUrl=%{BASE_URL}% +#baguette.client.install.downloadUrl=http://${EMS_SERVER_ADDRESS}:8111/resources +baguette.client.install.apiKey=${web.security.api-key-authentication.value} +baguette.client.install.installScriptUrl=${baguette.client.install.downloadUrl}/install.sh +baguette.client.install.installScriptFile=${baguette.client.install.baseDir}/bin/install.sh + +### --- Archive copying --- +#baguette.client.install.archiveSourceDir=${EMS_CONFIG_DIR}/baguette-client +#baguette.client.install.archiveDir=${EMS_CONFIG_DIR}/baguette-client +#baguette.client.install.archiveFile=baguette-client-conf.tgz +#baguette.client.install.clientConfArchiveFile=${baguette.client.install.baseDir}/baguette-client-conf.tgz + +### --- EMS server (HTTPS) certificate file (PEM) --- +#baguette.client.install.serverCertFileAtServer=${EMS_CONFIG_DIR}/baguette-client/conf/server.pem +baguette.client.install.serverCertFileAtServer=${EMS_CONFIG_DIR}/server.pem +baguette.client.install.serverCertFileAtClient=${baguette.client.install.baseDir}/conf/server.pem +baguette.client.install.copyFilesFromServerDir=${EMS_CONFIG_DIR}/baguette-client/ +baguette.client.install.copyFilesToClientDir=${baguette.client.install.baseDir}/ + +### --- temp. folders --- +baguette.client.install.clientTmpDir=/tmp +#baguette.client.install.serverTmpDir=${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/tmp +baguette.client.install.serverTmpDir=${EMS_HOME}/tmp +baguette.client.install.keepTempFiles=false + +### Simulation settings +#baguette.client.install.simulate-connection = true +#baguette.client.install.simulate-execution = true + +### SSH connection settings +#baguette.client.install.maxRetries = 5 +#baguette.client.install.retryDelay = 1000 +#baguette.client.install.retryBackoffFactor = 1.0 +#baguette.client.install.connectTimeout = 10000 +#baguette.client.install.authenticateTimeout = 60000 +#baguette.client.install.heartbeatInterval = 60000 +#baguette.client.install.commandExecutionTimeout = 60000, + +### ----------------------------------------- +### Instruction Set file processing settings + +baguette.client.install.instructions.LINUX = \ + file:${EMS_CONFIG_DIR}/baguette-client-install/linux/check-ignore.json, \ + file:${EMS_CONFIG_DIR}/baguette-client-install/linux/detect.json, \ + file:${EMS_CONFIG_DIR}/baguette-client-install/linux/netdata.json, \ + file:${EMS_CONFIG_DIR}/baguette-client-install/linux/jre.json, \ + file:${EMS_CONFIG_DIR}/baguette-client-install/linux/baguette.json, \ + file:${EMS_CONFIG_DIR}/baguette-client-install/linux/baguette-skip.json, \ + file:${EMS_CONFIG_DIR}/baguette-client-install/linux/start-agents.json +baguette.client.install.instructions.WINDOWS = file:${EMS_CONFIG_DIR}/baguette-client-install/win/win.json + +baguette.client.install.continueOnFail = true +baguette.client.install.sessionRecordingDir = ${LOGS_DIR:${EMS_CONFIG_DIR}/../logs} + +### Baguette and Netdata installation parameters (for condition checking) + +#baguette.client.install.parameters.SKIP_IGNORE_CHECK=true +#baguette.client.install.parameters.SKIP_DETECTION=true +#baguette.client.install.parameters.SKIP_NETDATA_INSTALLATION=true +#baguette.client.install.parameters.SKIP_BAGUETTE_INSTALLATION=true +#baguette.client.install.parameters.SKIP_JRE_INSTALLATION=true +#baguette.client.install.parameters.SKIP_START=true + +baguette.client.install.parameters.BAGUETTE_INSTALLATION_MIN_PROCESSORS=2 +baguette.client.install.parameters.BAGUETTE_INSTALLATION_MIN_RAM=2*1024*1024 +baguette.client.install.parameters.BAGUETTE_INSTALLATION_MIN_DISK_FREE=1024*1024 + +### Settings for resolving Node state after baguette client installation +#baguette.client.install.clientInstallVarName=__EMS_CLIENT_INSTALL__ +#baguette.client.install.clientInstallSuccessPattern=^INSTALLED($|[\s:=]) +#baguette.client.install.clientInstallErrorPattern=^ERROR($|[\s:=]) + +#baguette.client.install.skipInstallVarName=__EMS_CLIENT_INSTALL__ +#baguette.client.install.skipInstallPattern=^SKIPPED($|[\s:=]) + +#baguette.client.install.ignoreNodeVarName=__EMS_IGNORE_NODE__ +#baguette.client.install.ignoreNodePattern=^IGNORED($|[\s:=]) + +#baguette.client.install.ignoreNodeIfVarIsMissing=false +#baguette.client.install.skipInstallIfVarIsMissing=false +#baguette.client.install.clientInstallSuccessIfVarIsMissing=false +#baguette.client.install.clientInstallErrorIfVarIsMissing=true + +baguette.client.install.installationContextProcessorPlugins=\ + plugin.install.gr.iccs.imu.ems.baguette.client.AllowedTopicsProcessorPlugin, \ + plugin.install.gr.iccs.imu.ems.baguette.client.PrometheusProcessorPlugin + +### Server-side Self-Healing. Recovers monitoring functionality of registered nodes (i.e. EMS client and/or Netdata agent) +self.healing.enabled=true +self.healing.mode=INCLUDED +self.healing.recovery.delay=10000 +self.healing.recovery.retryDelay=60000 +self.healing.recovery.maxRetries=3 +self.healing.recovery.file.baguette=file:${EMS_CONFIG_DIR}/baguette-client-install/linux/recover-baguette.json +self.healing.recovery.file.netdata= + + +################################################################################ +### EMS - CAMEL-to-EPL Translator properties ### +################################################################################ + +### Translator configuration +#translator.translatorType=CAMEL_FILE +#translator.translatorProperties.camelFile.modelsDir=/models/ +#translator.translatorProperties.camelWeb.baseUrl=http://models-server:8080/ +#translator.translatorProperties.camelWeb.modelsDir=/models/web +#translator.translatorProperties.camelWeb.deleteFile=false + +translator.leaf-node-grouping = PER_INSTANCE +translator.prune-mvv=true +translator.add-top-level-metrics=true + +### IMPORTANT: Pattern must yield valid EPL identifiers +translator.full-name-pattern={TYPE}__{CAMEL}__{MODEL}__{ELEM}__{COUNT} +translator.formula-check-enabled=true + +### Sensor settings +translator.sensor-configuration-annotation=MELODICMetadataSchema.ContextAwareSecurityModel.SecurityContextElement.Object.DataArtefact.Configuration.ConfigurationFormat.JSON_FORMAT +translator.sensor-min-interval=1 +translator.sensor-default-interval=60 + +# Load-annotated metric settings +translator.loadMetricAnnotation=MELODICMetadataSchema.UtilityNotions.UtilityRelatedProperties.Utility.BusyInstanceMetric +translator.loadMetricVariableFormatter=busy.%s + +### Print results and export switches +#translator.print-results=true +translator.dag.export-to-dot-enabled=false +translator.dag.export-to-file-enabled=false + +### Graph rendering parameters +translator.dag.export-path=${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/exports +#translator.dag.export-formats=png,svg,xdot,ps,json,plain,plain_ext +#translator.dag.export-formats=png,svg,xdot +translator.dag.export-formats=png,svg +translator.dag.export-image-width=600 + +### Active sinks (list) +#translator.active-sinks=JMS +# +### Sink configurations +#translator.sink-config.JMS.jms.broker=failover:(tcp://localhost:61616)?initialReconnectDelay=1000&warnAfterReconnectAttempts=10 +#translator.sink-config.JMS.jms.topic.selector=de.uniulm.omi.cloudiator.visor.reporting.jms.MetricNameTopicSelector +#translator.sink-config.JMS.jms.message.format=de.uniulm.omi.cloudiator.visor.reporting.jms.MelodicJsonEncoding + +################################################################################ \ No newline at end of file diff --git a/ems-core/config-files/ems-server.yml b/ems-core/config-files/ems-server.yml new file mode 100644 index 0000000..b5cde30 --- /dev/null +++ b/ems-core/config-files/ems-server.yml @@ -0,0 +1,666 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +################################################################################ +### Global settings +################################################################################ + +### Don't touch the next line!! +EMS_SERVER_ADDRESS: ${${control.IP_SETTING}} +DOLLAR: '$' + +### Password Encoder settings +#password-encoder-class: gr.iccs.imu.ems.util.password.AsterisksPasswordEncoder +#password-encoder-class: gr.iccs.imu.ems.util.password.IdentityPasswordEncoder +#password-encoder-class: gr.iccs.imu.ems.util.password.PresentPasswordEncoder + +### Jasypt encryptor settings (using old settings until encrypted texts are updated) +jasypt: + encryptor: + algorithm: PBEWithMD5AndDES + ivGeneratorClassname: org.jasypt.iv.NoIvGenerator + +### Execution (@EnableAsync) and Scheduling (@EnableScheduling) thread pools +#spring.task.execution.pool.max-size: 16 +#spring.task.execution.pool.queue-capacity: 100 +#spring.task.execution.pool.keep-alive: '10s' +#spring.task.scheduling.pool.size: 2 + +### Misc +spring.output.ansi.enabled: ALWAYS +spring.jackson.default-property-inclusion: non_null + + +################################################################################ +### Web server port and TLS settings +################################################################################ + +server: + port: 8111 + ssl: + enabled: true + ### Keystore/Truststore settings + key-store: ${control.ssl.keystore-file} + key-store-password: ${control.ssl.keystore-password} + key-store-type: ${control.ssl.keystore-type} + key-alias: ${control.ssl.key-entry-name} + #key-password: ${control.ssl.key-entry-password} + # + ### SSL ciphers and protocol settings + #ciphers: TLS_RSA_WITH_AES_128_CBC_SHA256 # SSL ciphers + #protocol: TLS # SSL protocol to use + #enabled-protocols: TLSv1.2 # Enabled SSL protocols + +#security.require-ssl: true + + +################################################################################ +### JWT settings +jwt: + secret: ENC(I0mRWgH2FVDDNs4OBcdh7Z+o3lOQDa3ztaEtmnXT2HN0aClkChp/lqm9zM5HyTk0stJ7v2Di75U=) +# expirationTime: 86400000 +# refreshTokenExpirationTime: 86400000 + + +################################################################################ +### Authorization settings +### NOTE: More authorization settings in 'authorization-client.properties' +authorization: + enabled: false + #paths-protected: [ '/camelModel*', '/cpModel*', '/ems/**', '/baguette/**', '/event/**', '/monitors' ] + #paths-excluded: [] + + +################################################################################ +### Logback configuration file +logging: + config: file:${EMS_CONFIG_DIR}/logback-conf/logback-spring.xml + + +################################################################################ +### Web Log-viewer configuration +log-viewer: + url-mapping: /log-viewer + + +################################################################################ +### EMS - Control Service properties ### +################################################################################ + +control: + + ### Don't touch the next lines!! + IP_SETTING: ${EMS_IP_SETTING:PUBLIC_IP} + EXECUTIONWARE: PROACTIVE + + ### URLs of Upperware services being invoked by EMS + esb-url: ${ESB_URL:https://mule:8088} + metasolver-configuration-url: ${METASOLVER_URL:http://metasolver:8092/updateConfiguration} + + ### Log settings + #print-build-info: true + log-requests: ${EMS_LOG_REQUESTS:false} + + ### Debug settings - Deactivate processing modules + #skip-translation: true + #skip-mvv-retrieve: true + #skip-broker-cep: true + #skip-baguette: true + #skip-collectors: true + #skip-metasolver: true + #skip-esb-notification: true + upperware-grouping: GLOBAL + + ### Debug settings - Load/Save translation results + tc-load-file: ${EMS_TC_LOAD_FILE:${EMS_TC_FILE:${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/_TC.json}} + tc-save-file: ${EMS_TC_SAVE_FILE:${EMS_TC_FILE:${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/_TC.json}} + + ### Process CAMEL and CP models on start-up + preload: + ### CAMEL model to process on start-up + camel-model: ${EMS_PRELOAD_CAMEL_MODEL:} + ### CP model to process on start-up + cp-model: ${EMS_PRELOAD_CP_MODEL:} + + ### Exit settings + exit-allowed: false + exit-grace-period: 10 + exit-code: 0 + + ### Key store, Trust store and Certificate settings + ssl: + # Key store settings + keystore-file: ${EMS_CONFIG_DIR}/ems-keystore.p12 + keystore-type: PKCS12 + #keystore-password: 'ENC(ISMbn01HVPbtRPkqm2Lslg==)' # melodic + + # Trust store settings + truststore-file: ${EMS_CONFIG_DIR}/ems-truststore.p12 + truststore-type: PKCS12 + #truststore-password: 'ENC(ISMbn01HVPbtRPkqm2Lslg==)' # melodic + + # Certificate settings + certificate-file: ${EMS_CONFIG_DIR}/ems-cert.crt + + # EMS key generation settings + key-entry-generate: ALWAYS + key-entry-name: ems + #key-entry-password: + key-entry-dname: 'CN=ems,OU=Information Management Unit (IMU),O=Institute of Communication and Computer Systems (ICCS),L=Athens,ST=Attika,C=GR' + key-entry-ext-san: 'dns:localhost,ip:127.0.0.1,ip:${DEFAULT_IP},ip:${PUBLIC_IP}' + +################################################################################ +### Web configuration - Static resources + +### Static Web Resources and Redirects +web.static: + + ### Favicon settings + #favicon-context: /favicon.ico + favicon-path: file:${PUBLIC_DIR}/favicon.ico + + ### Static resource settings + resource-context: /** + resource-path: file:${PUBLIC_DIR}/ + + logs-context: /logs/** + logs-path: file:${LOGS_DIR}/ + + ### Redirects + #redirect: /resources/index.html + redirects: + '[/]': '/admin/index.html' + '[/index.html]': '/admin/index.html' + '[/admin]': '/admin/index.html' + '[/admin/]': '/admin/index.html' + '[/resources]': '/resources/index.html' + '[/resources/]': '/resources/index.html' + +################################################################################ +### Web & REST Security configuration + +### NOTE: Setting this to 'false' will turn off all security features +#melodic.security.enabled: false + +#web.security: +# +# ### JWT authentication ### +# jwt-authentication: +# enabled: false +# request-parameter: jwt +# print-sample-token: false +# +# ### API Key access ### +# api-key-authentication: +# enabled: false +# #value: ${random.uuid} +# value: 1234567890 +# request-header: EMS-API-KEY +# request-parameter: ems-api-key +# +# ### OTP access ### +# otp-authentication: +# enabled: false +# duration: 3600000 +# request-header: EMS-OTP +# request-parameter: ems-otp +# +# ### User Web Form authentication ### +# form-authentication: +# enabled: false +# username: admin +# password: ems + +################################################################################ +### Topic Beacon settings +beacon: + enabled: true + initial-delay: 60000 + delay: 60000 + #rate: 60000 + #use-delay: false + heartbeat-topics: + threshold-topics: _ui_threshold_info + instance-topics: _ui_instance_info + prediction-topics: metrics_to_predict + prediction-rate: 60000 + slo-violation-detector-topics: metric.metric_list + +################################################################################ +### Info Service settings +info: + metrics-update-interval: 1000 + metrics-client-update-interval: 10000 + metrics-stream-update-interval: 10 # in seconds + metrics-stream-event-name: ems-metrics-event + env-var-prefixes: + - WEBSSH_SERVICE_-^ + - WEB_ADMIN_!^ + # ! at the end means to trim off the prefix; - at the end means to convert '_' to '-'; + # ^ at the end means convert to upper case; ~ at the end means convert to lower case; + +################################################################################ +### Collectors settings +collector: + netdata: + enable: true + delay: 10000 + skipLocal: true + url: http://127.0.0.1:19999/api/v1/allmetrics?format=json + urlOfNodesWithoutClient: http://%s:19999/api/v1/allmetrics?format=json + #create-topic: true + #allowed-topics: netdata__system__cpu__user:an_alias + error-limit: 3 + pause-period: 60 + +################################################################################ +### Management and Endpoint settings +management: + info: + build.enabled: true + env.enabled: true + git.enabled: true + java.enabled: true + endpoints.web: + exposure.include: 'health,info' +# exposure.include: 'health,info,hawtio,jolokia' +# base-path: / +# endpoint.health.show-details: always +# security.enabled: false +# port: 9001 +# address: 127.0.0.1 +#endpoints.metrics.sensitive: false + +### Hawtio web console settings +#management.endpoints.web.path-mapping.hawtio: hawtio/console +#hawtio: +# authenticationEnabled: false # NOTE: Uncomment to enable actuator and hawtio +# proxyWhitelist: +# realm: hawtio +# role: admin,viewer +# rolePrincipalClasses: org.apache.activemq.jaas.GroupPrincipal + +### Jolokia (HTTP-JMX bridge) settings +#jolokia.config.debug: false +#endpoints.jolokia: +# enabled: true +# sensitive: false +# path: /jolokia +#spring.jmx.enabled: true +#endpoints.jmx.enabled: true + +################################################################################ +### Spring Boot Admin Client settings +#spring.boot.admin.client: +# url: http://localhost:8080 +# username: username +# password: password +# instance.service-base-url: http://localhost:8080 + + +################################################################################ +### EMS - Broker-CEP properties ### +################################################################################ + +BROKER_URL_PROPERTIES: transport.daemon=true&transport.trace=false&transport.useKeepAlive=true&transport.useInactivityMonitor=false&transport.needClientAuth=${CLIENT_AUTH_REQUIRED}&transport.verifyHostName=true&transport.connectionTimeout=0&transport.keepAlive=true +CLIENT_AUTH_REQUIRED: false +CLIENT_URL_PROPERTIES: daemon=true&trace=false&useInactivityMonitor=false&connectionTimeout=0&keepAlive=true + +brokercep: + # Broker name, ports and protocol + #broker-name: broker + broker-port: 61617 + broker-protocol: ssl + #management-connector-port: 1099 + #bypass-local-broker: true # Don't use in EMS server + + # Broker connectors + broker-url: + - ${brokercep.broker-protocol}://0.0.0.0:${brokercep.broker-port}?${BROKER_URL_PROPERTIES} + - tcp://0.0.0.0:61616?${BROKER_URL_PROPERTIES} + - stomp://0.0.0.0:61610 + + # Broker URLs for (EMS) consumer and clients + broker-url-for-consumer: tcp://${EMS_SERVER_ADDRESS}:61616?${CLIENT_URL_PROPERTIES} + broker-url-for-clients: ${brokercep.broker-protocol}://${EMS_SERVER_ADDRESS}:${brokercep.broker-port}?${CLIENT_URL_PROPERTIES} + # Must be a public IP address + + ssl: + # Key store settings + keystore-file: ${EMS_CONFIG_DIR}/broker-keystore.p12 + keystore-type: ${control.ssl.keystore-type} + keystore-password: ${control.ssl.keystore-password} + + # Trust store settings + truststore-file: ${EMS_CONFIG_DIR}/broker-truststore.p12 + truststore-type: ${control.ssl.truststore-type} + truststore-password: ${control.ssl.truststore-password} + + # Certificate settings + certificate-file: ${EMS_CONFIG_DIR}/broker.crt + + # EMS key generation settings + key-entry-generate: ALWAYS + key-entry-name: ${control.ssl.key-entry-name} + key-entry-dname: ${control.ssl.key-entry-dname} + key-entry-ext-san: ${control.ssl.key-entry-ext-san} + + # Authentication and Authorization settings + authentication-enabled: true + #additional-broker-credentials: aaa/111, bbb/222, morphemic/morphemic + #additional-broker-credentials: ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ) + authorization-enabled: false + + # Broker instance settings + broker-persistence-enabled: false + broker-using-jmx: true + broker-advisory-support-enabled: true + broker-using-shutdown-hook: false + + broker-enable-statistics: true + broker-populate-jmsx-user-id: true + + # Message interceptors + message-interceptors: + - destination: '>' + className: 'gr.iccs.imu.ems.brokercep.broker.interceptor.SequentialCompositeInterceptor' + params: + - '#SourceAddressMessageUpdateInterceptor' + - '#LogMessageUpdateInterceptor' + - '#MessageForwarderInterceptor' + + message-interceptors-specs: + SourceAddressMessageUpdateInterceptor: + className: gr.iccs.imu.ems.brokercep.broker.interceptor.SourceAddressMessageUpdateInterceptor + LogMessageUpdateInterceptor: + className: gr.iccs.imu.ems.brokercep.broker.interceptor.LogMessageUpdateInterceptor + MessageForwarderInterceptor: + className: gr.iccs.imu.ems.brokercep.broker.interceptor.MessageForwarderInterceptor + + # Message forward destinations (MessageForwarderInterceptor must be included in 'message-interceptors' property) + #message-forward-destinations: + # - connection-string: tcp://localhost:51515 + # username: AAA + # password: 111 + # - connection-string: tcp://localhost:41414 + # username: AAA + # password: 111 + + # Advisory watcher + enable-advisory-watcher: true + + # Memory usage limit + usage: + memory: + jvm-heap-percentage: 20 + #size: 134217728 + + # Event forward settings + #maxEventForwardRetries: -1 + #maxEventForwardDuration: -1 + + # Event recorder settings + event-recorder: + enabled: true + #format: JSON + file: ${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/events-%T.%S + #filter-mode: ALL | REGISTERED (default) | ALLOWED + #allowed-destinations: + + +################################################################################ +### EMS - Baguette Server properties ### +################################################################################ + +baguette.server: + + # Coordinator settings - Old style + coordinator-class: gr.iccs.imu.ems.baguette.server.coordinator.cluster.ClusteringCoordinator + #coordinatorParameters: + # param1: p1 + # param2: p2 + + # Coordinator settings - New style + coordinator-id: [ clustering, 2level, noop ] + coordinatorConfig: + clustering: + coordinatorClass: gr.iccs.imu.ems.baguette.server.coordinator.cluster.ClusteringCoordinator + parameters: + zone-management-strategy-class: gr.iccs.imu.ems.baguette.server.coordinator.cluster.DefaultZoneManagementStrategy + zone-port-start: 2000 + zone-port-end: 2999 + zone-keystore-file-name-formatter: '${LOGS_DIR:logs}/cluster_${DOLLAR}{TIMESTAMP}_${DOLLAR}{ZONE_ID}.p12' + #cluster-detector-class: gr.iccs.imu.ems.baguette.server.coordinator.cluster.ClusterZoneDetector + #cluster-detector-rules-type: MAP + #cluster-detector-rules-separator: ',' + #cluster-detector-rules: zone, zone-id, region, region-id, cloud, cloud-id, provider, provider-id + #default-clusters: DEFAULT_CLUSTER_A, DEFAULT_CLUSTER_B + #assignment-to-default-clusters: RANDOM + 2level: + coordinatorClass: gr.iccs.imu.ems.baguette.server.coordinator.TwoLevelCoordinator + noop: + coordinatorClass: gr.iccs.imu.ems.baguette.server.coordinator.NoopCoordinator + + # Registration settings + #number-of-instances: 1 + registration-window: 30000 + + # SSH Server settings + address: ${EMS_SERVER_ADDRESS} + port: 2222 + key-file: ${EMS_CONFIG_DIR}/hostkey.pem + heartbeat-enabled: true + heartbeat-period: 60000 + + # SSH Server additional username/passwords + #credentials: + # aa: xx + # bb: yy + + # Client Id generation settings + #client-address-override-allowed: true + client-id-format-escape: '~' + client-id-format: '~{type:-_}-~{operatingSystem:-_}-~{id:-_}-~{name:-_}-~{provider:-_}-~{address:-_}-~{random:-_}' + + +################################################################################ +### EMS - Baguette Client Install properties ### +################################################################################ + +baguette.client.install: + + ### OS families + osFamilies: + LINUX: [ UNKNOWN_OS_FAMILY, CENTOS,DARWIN,DEBIAN,FEDORA ,FREEBSD ,GENTOO,COREOS,AMZN_LINUX,MANDRIVA ,NETBSD,OEL ,OPENBSD,RHEL,SCIENTIFIC,CEL,SLACKWARE,SOLARIS,SUSE,TURBOLINUX,CLOUD_LINUX,UBUNTU ] + WINDOWS: [ WINDOWS ] + + ### Workers + workers: 5 + + ### Installation settings + ### --- Root command --- + ### E.g. 'echo ${NODE_SSH_PASSWORD} | sudo -S -- ' + rootCmd: '' + + ### --- Directories and files --- + baseDir: ~/baguette-client + mkdirs: [ '${baguette.client.install.baseDir}/bin', '${baguette.client.install.baseDir}/conf', '${baguette.client.install.baseDir}/logs' ] + touchFiles: [ '${baguette.client.install.baseDir}/logs/output.txt' ] + checkInstalledFile: ${baguette.client.install.baseDir}/conf/ok.txt + + ### --- Installation script URL and file (obsolete) --- + downloadUrl: '%{BASE_URL}%' + #downloadUrl: http://${EMS_SERVER_ADDRESS}:8111/resources + apiKey: ${web.security.api-key-authentication.value} + installScriptUrl: ${baguette.client.install.downloadUrl}/install.sh + installScriptFile: ${baguette.client.install.baseDir}/bin/install.sh + + ### --- Archive copying --- + #archiveSourceDir: ${EMS_CONFIG_DIR}/baguette-client + #archiveDir: ${EMS_CONFIG_DIR}/baguette-client + #archiveFile: baguette-client-conf.tgz + #clientConfArchiveFile: ${baguette.client.install.baseDir}/baguette-client-conf.tgz + + ### --- EMS server (HTTPS) certificate file (PEM) --- + #serverCertFileAtServer: ${EMS_CONFIG_DIR}/baguette-client/conf/server.pem + serverCertFileAtServer: ${EMS_CONFIG_DIR}/server.pem + serverCertFileAtClient: ${baguette.client.install.baseDir}/conf/server.pem + copyFilesFromServerDir: ${EMS_CONFIG_DIR}/baguette-client/ + copyFilesToClientDir: ${baguette.client.install.baseDir}/ + + ### --- temp. folders --- + clientTmpDir: /tmp + #serverTmpDir: ${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/tmp + serverTmpDir: ${EMS_HOME}/tmp + keepTempFiles: false + + ### Simulation settings + #simulateConnection: false + #simulateExecution: false + + ### SSH connection settings + #maxRetries: 5 + #retryDelay: 1000 + #retryBackoffFactor: 1.0 + #connectTimeout: 10000 + #authenticateTimeout: 60000 + #heartbeatInterval: 60000 + #commandExecutionTimeout: 60000, + + ### ----------------------------------------- + ### Instruction Set file processing settings + + instructions: + LINUX: + # Instructions set files - JSON version + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/check-ignore.json + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/detect.json + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/netdata.json + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/baguette-remove.json + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/jre.json + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/baguette.json + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/baguette-skip.json + - file:${EMS_CONFIG_DIR}/baguette-client-install/linux/start-agents.json + # Instructions set files - YAML version +# - file:${EMS_CONFIG_DIR}/baguette-client-install/linux-yaml/check-ignore.yml +# - file:${EMS_CONFIG_DIR}/baguette-client-install/linux-yaml/detect.yml +# - file:${EMS_CONFIG_DIR}/baguette-client-install/linux-yaml/netdata.yml +# - file:${EMS_CONFIG_DIR}/baguette-client-install/linux-yaml/jre8.yml +# - file:${EMS_CONFIG_DIR}/baguette-client-install/linux-yaml/baguette.yml +# - file:${EMS_CONFIG_DIR}/baguette-client-install/linux-yaml/baguette-skip.yml +# - file:${EMS_CONFIG_DIR}/baguette-client-install/linux-yaml/start-agents.yml + WINDOWS: + - file:${EMS_CONFIG_DIR}/baguette-client-install/win/win.json + + continueOnFail: true + sessionRecordingDir: ${LOGS_DIR:${EMS_CONFIG_DIR}/../logs} + + ### Baguette and Netdata installation parameters (for condition checking) + parameters: + + #SKIP_IGNORE_CHECK: true + #SKIP_DETECTION: true + #SKIP_NETDATA_INSTALLATION: true + #SKIP_BAGUETTE_INSTALLATION: true + #SKIP_JRE_INSTALLATION: true + #SKIP_START: true + + BAGUETTE_INSTALLATION_MIN_PROCESSORS: 2 + BAGUETTE_INSTALLATION_MIN_RAM: 2*1024*1024 + BAGUETTE_INSTALLATION_MIN_DISK_FREE: 1024*1024 + + ### Settings for resolving Node state after baguette client installation + #clientInstallVarName: '__EMS_CLIENT_INSTALL__' + #clientInstallSuccessPattern: '^INSTALLED($|[\s:=])' + #clientInstallErrorPattern: '^ERROR($|[\s:=])' + # + #skipInstallVarName: '__EMS_CLIENT_INSTALL__' + #skipInstallPattern: '^SKIPPED($|[\s:=])' + # + #ignoreNodeVarName: '__EMS_IGNORE_NODE__' + #ignoreNodePattern: '^IGNORED($|[\s:=])' + # + #ignoreNodeIfVarIsMissing: false + #skipInstallIfVarIsMissing: false + #clientInstallSuccessIfVarIsMissing: false + #clientInstallErrorIfVarIsMissing: true + + installationContextProcessorPlugins: + - gr.iccs.imu.ems.baguette.client.install.plugin.AllowedTopicsProcessorPlugin + - gr.iccs.imu.ems.baguette.client.install.plugin.PrometheusProcessorPlugin + +### Server-side Self-Healing. Recovers monitoring functionality of registered nodes (i.e. EMS client and/or Netdata agent) +self.healing: + enabled: true + mode: INCLUDED + recovery: + delay: 10000 + retryDelay: 60000 + maxRetries: 3 + file: + baguette: file:${EMS_CONFIG_DIR}/baguette-client-install/linux/recover-baguette.json + netdata: + + +################################################################################ +### EMS - CAMEL-to-EPL Translator properties ### +################################################################################ + +### Translator configuration +translator: + #translatorType: CAMEL_FILE + #translatorProperties: + # camelFile: + # modelsDir: /models/ + # camelWeb: + # baseUrl: http://models-server:8080/ + # modelsDir: /models/web + # deleteFile: false + + leaf-node-grouping: PER_INSTANCE + prune-mvv: true + add-top-level-metrics: true + + ### IMPORTANT: Pattern must yield valid EPL identifiers + full-name-pattern: '{TYPE}__{CAMEL}__{MODEL}__{ELEM}__{COUNT}' + formula-check-enabled: true + + ### Sensor settings + sensor-configuration-annotation: 'MELODICMetadataSchema.ContextAwareSecurityModel.SecurityContextElement.Object.DataArtefact.Configuration.ConfigurationFormat.JSON_FORMAT' + sensor-min-interval: 1 + sensor-default-interval: 60 + + # Load-annotated metric settings + loadMetricAnnotation: 'MELODICMetadataSchema.Application_Placement_Model.UtilityNotions.BusyInstanceMetric' + loadMetricVariableFormatter: 'busy.%s' + + ### Print results and export switches + #print-results: true + dag: + export-to-dot-enabled: false + export-to-file-enabled: false + + ### Graph rendering parameters + export-path: ${LOGS_DIR:${EMS_CONFIG_DIR}/../logs}/exports + #export-formats: [ 'png', 'svg', 'xdot', 'ps', 'json', 'plain', 'plain_ext' ] + #export-formats: [ 'png', 'svg', 'xdot' ] + export-formats: [ 'png', 'svg' ] + export-image-width: 600 + + ### Active sinks (list) + #sinks: [ 'JMS' ] + # + ### Sink configurations + #sink-config: + # JMS: + # jms.broker: 'failover:(tcp://localhost:61616)?initialReconnectDelay=1000&warnAfterReconnectAttempts=10' + # jms.topic.selector: 'de.uniulm.omi.cloudiator.visor.reporting.jms.MetricNameTopicSelector' + # jms.message.format: 'de.uniulm.omi.cloudiator.visor.reporting.jms.MelodicJsonEncoding' + +################################################################################ \ No newline at end of file diff --git a/ems-core/config-files/eu.melodic.upperware.security.properties b/ems-core/config-files/eu.melodic.upperware.security.properties new file mode 100644 index 0000000..8559c75 --- /dev/null +++ b/ems-core/config-files/eu.melodic.upperware.security.properties @@ -0,0 +1,17 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# JWT authentication configuration for EMS +jwt.secret=mE1odiCl0ud +jwt.expirationTime=86400000 +jwt.refreshTokenExpirationTime=86400000 + +#melodic user +user.username=myUsername +user.password=myPassword \ No newline at end of file diff --git a/ems-core/config-files/eu.paasage.mddb.cdo.client.properties b/ems-core/config-files/eu.paasage.mddb.cdo.client.properties new file mode 100644 index 0000000..63f31f4 --- /dev/null +++ b/ems-core/config-files/eu.paasage.mddb.cdo.client.properties @@ -0,0 +1,29 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +######################################################### +# cdo server connection properties +# This file is used for development. During production +# run, there should be a similar file located in the +# $PAASAGE_CONFIG_DIR and which is used by the +# upperware components. +######################################################### + +###CDO server endpoint +#host=127.0.0.1 +host=cdo-server + +###server port +port=2036 + +###repository name +repository=repo1 + +#logging to be set off or on - default is off +logging=off diff --git a/ems-core/config-files/logback-conf/logback-spring.xml b/ems-core/config-files/logback-conf/logback-spring.xml new file mode 100644 index 0000000..63c030f --- /dev/null +++ b/ems-core/config-files/logback-conf/logback-spring.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ems-core/config-files/secrets.properties b/ems-core/config-files/secrets.properties new file mode 100644 index 0000000..1184da4 --- /dev/null +++ b/ems-core/config-files/secrets.properties @@ -0,0 +1,28 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +### EMS server secrets +web.security.api-key-authentication.value=1234567890 +web.security.form-authentication.password=ems + +### EMS Server Keystore/Truststore password: melodic +control.ssl.keystore-password=ENC(ISMbn01HVPbtRPkqm2Lslg==) +control.ssl.truststore-password=ENC(ISMbn01HVPbtRPkqm2Lslg==) + +### Additional ActiveMQ Broker credentials: aaa/111, bbb/222, morphemic/morphemic +#brokercep.additional-broker-credentials=ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ) + +### Additional Baguette Server SSH username/passwords: aa/xx, bb/yy +#baguette.server.credentials.aa=xx +#baguette.server.credentials.bb=yy + +### Other settings +control.IP_SETTING=DEFAULT_IP +control.esb-url= +control.metasolver-configuration-url= diff --git a/ems-core/control-service/pom.xml b/ems-core/control-service/pom.xml new file mode 100644 index 0000000..4a39e83 --- /dev/null +++ b/ems-core/control-service/pom.xml @@ -0,0 +1,721 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + control-service + EMS - Control Service + + + + 3.1.3 + 1.11.2 + 2.1.0 + 0.11.5 + + ${maven.build.timestamp} + yyyy-MM-dd HH:mm:ss + + ${project.build.finalName}.jar + esper-${esper.version}.jar + + + 0.43.2 + ems-server + + + + gr.iccs.imu.ems.control.ControlServiceApplication + + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + gr.iccs.imu.ems + baguette-server + ${project.version} + + + gr.iccs.imu.ems + baguette-client-install + ${project.version} + + + gr.iccs.imu.ems + broker-cep + ${project.version} + + + gr.iccs.imu.ems + common + ${project.version} + + + gr.iccs.imu.ems + translator + ${project.version} + + + gr.iccs.imu.ems + util + ${project.version} + + + gr.iccs.imu.ems + broker-client + ${project.version} + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.micrometer + micrometer-registry-prometheus + ${micrometer.registry.prometheus.version} + + + + de.codecentric + spring-boot-admin-starter-client + ${spring.boot.admin.version} + + + + com.github.ulisesbocchio + jasypt-spring-boot-starter + ${jasypt.starter.version} + + + + + org.projectlombok + lombok + provided + + + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + + org.apache.commons + commons-lang3 + + + + + + + + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + org.springframework.boot + * + + + + + org.springdoc + springdoc-openapi-starter-webflux-ui + ${springdoc.version} + + + org.springframework.boot + * + + + + + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + 1.11 + + + org.bouncycastle + * + + + org.codehaus.plexus + plexus-utils + + + com.google.guava + guava + + + commons-io + commons-io + + + + + org.codehaus.plexus + plexus-utils + 4.0.0 + + + commons-io + commons-io + 2.13.0 + + + + + + ${project.artifactId} + + + src/main/resources + true + + *.txt + META-INF/spring.factories + + + + + + + maven-clean-plugin + 3.3.1 + + + remove-old-public-resources + clean + + clean + + + + + + + + ${project.parent.basedir}/public_resources + + **/* + + false + + + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + 6.0.0 + + + + org.apache.maven.plugins + maven-jar-plugin + + + original-jar + + jar + + + original + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + gr.iccs.imu.ems + baguette-client + + + com.espertech + esper + + + + + + build-info + + build-info + + + + ${timestamp} + ${buildNumber} + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-esper-jar + prepare-package + + copy + + + + + com.espertech + esper + ${esper.version} + jar + ${project.build.directory} + + + gr.iccs.imu.ems + baguette-client + ${project.version} + tgz + installation-package + ${project.build.directory} + baguette-client-installation-package.tgz + + + + + + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + 1.11 + + + package + + files + + + + + + + ${project.build.directory} + + + *.zip + *.tar + *.tar.gz + *.tgz + + + *.jar + + + + + MD5 + + true + true + false + + + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + 1.0 + + + copy-files + package + + copy + + + + + + ${project.basedir}/src/main/resources/public/index.html + ../public_resources/resources/index.html + + + ${project.basedir}/src/main/resources/public/favicon.ico + ../public_resources/favicon.ico + + + + + ${project.build.directory}/checksums.csv + ../public_resources/resources/checksums.csv + + + + + ../broker-client/target/broker-client-jar-with-dependencies.jar + ../public_resources/resources/broker-client.jar + + + ${project.basedir}/src/main/resources/public/client.sh + ../public_resources/resources/client.sh + + + ${project.basedir}/src/main/resources/public/client.bat + ../public_resources/resources/client.bat + + + + + ${project.build.directory}/baguette-client-installation-package.tgz + ../public_resources/resources/baguette-client.tgz + + + ${project.build.directory}/baguette-client-installation-package.tgz.md5 + ../public_resources/resources/baguette-client.tgz.md5 + + + ../baguette-client/bin/install.sh + ../public_resources/resources/install.sh + + + true + true + + + + + + + maven-resources-plugin + 3.3.1 + + + + copy-web-admin-resources + + generate-resources + + copy-resources + + + ${project.parent.basedir}/public_resources/admin + + + ${project.parent.basedir}/web-admin/dist + false + + + + + + + + copy-resources-to-docker-context + package + + copy-resources + + + ${project.build.directory}/docker + + + ${project.basedir}/src/main/docker + + + + ${project.build.directory}/docker/jars + ${project.build.directory} + ${docker.controlServiceJar} + ${docker.esperJar} + false + + + ${project.build.directory}/docker/bin + ${project.basedir}/../bin/ + run.sh + sysmon.sh + detect.sh + jwtutil.sh + false + + + ${project.build.directory}/docker/config + ${project.basedir}/../config-files + + false + + + + + ${project.build.directory}/docker/public_resources + ${project.basedir}/../public_resources + false + + + + + ${project.build.directory}/docker/jars + ${project.basedir}/../broker-client/target + broker-client-jar-with-dependencies.jar + false + + + ${project.build.directory}/docker/bin + ${project.basedir}/../bin/ + client.sh + false + + + + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + + + + + + + dev-local-docker-image-build + + + ../.dev-local-docker-image-build + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.2.0 + + + validate + + read-project-properties + + + + + + + ../.dev-local-docker-image-build + + + + + + + + + + + build-docker-image + + . + + + + + + maven-antrun-plugin + 3.1.0 + + + set-docker-properties + validate + + run + + + true + + + + + + + + delete-old-docker-context + install + + run + + + + + + + + + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + 1.0.1 + + + rename-docker-context-dir + install + + rename + + + ${project.build.directory}/docker + ${project.build.directory}/docker-context + + + + + + + + io.fabric8 + docker-maven-plugin + ${docker-maven-plugin.version} + + + true + true + + + ${docker.image.name}:${docker.image.tag} + + ${project.build.directory}/docker-context + + + + + + + docker-image-build + install + + build + + + + + + + + + + + diff --git a/ems-core/control-service/src/main/docker/Dockerfile b/ems-core/control-service/src/main/docker/Dockerfile new file mode 100644 index 0000000..d75c94b --- /dev/null +++ b/ems-core/control-service/src/main/docker/Dockerfile @@ -0,0 +1,80 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +ARG BUILDER_IMAGE=eclipse-temurin:17.0.8_7-jre +ARG RUN_IMAGE=eclipse-temurin:17.0.8_7-jre + +# ----------------- Builder image ----------------- +FROM $BUILDER_IMAGE as ems-server-builder +#FROM vegardit/graalvm-maven:latest-java17 +WORKDIR /app +COPY jars/control-service.jar . +RUN java -Djarmode=layertools -jar control-service.jar extract + +# ----------------- Run image ----------------- +FROM $RUN_IMAGE + +# Setup environment +ENV BASEDIR /opt/ems-server +ENV EMS_HOME ${BASEDIR} +ENV EMS_CONFIG_DIR ${BASEDIR}/config + +ENV BIN_DIR ${BASEDIR}/bin +ENV CONFIG_DIR ${BASEDIR}/config +ENV LOGS_DIR ${BASEDIR}/logs +ENV PUBLIC_DIR ${BASEDIR}/public_resources + +# Install required and optional packages +RUN apt-get update && apt-get install -y \ + dumb-init \ + netcat \ + vim \ + iputils-ping \ + && rm -rf /var/lib/apt/lists/* + +# Add an EMS user +ARG EMS_USER=emsuser +RUN mkdir ${EMS_HOME} ; \ + addgroup ${EMS_USER} ; \ + adduser --home ${EMS_HOME} --no-create-home --ingroup ${EMS_USER} --disabled-password ${EMS_USER} ; \ + chown ${EMS_USER}:${EMS_USER} ${EMS_HOME} + +USER ${EMS_USER} +WORKDIR ${BASEDIR} + +# Download a JRE suitable for running EMS clients, and +# offer it for download +ENV JRE_LINUX_PACKAGE zulu17.44.15-ca-jre17.0.8-linux_x64.tar.gz +RUN mkdir -p ${PUBLIC_DIR}/resources +RUN curl https://cdn.azul.com/zulu/bin/${JRE_LINUX_PACKAGE} --output ${PUBLIC_DIR}/resources/${JRE_LINUX_PACKAGE} + +# Copy resource files to image +ADD --chown=${EMS_USER}:${EMS_USER} bin ${BIN_DIR} +ADD --chown=${EMS_USER}:${EMS_USER} config ${CONFIG_DIR} +ADD --chown=${EMS_USER}:${EMS_USER} public_resources ${PUBLIC_DIR} + +RUN mkdir ${LOGS_DIR} +RUN chmod +rx ${BIN_DIR}/*.sh + +# Copy files from builder container +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/dependencies ${BASEDIR} +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/spring-boot-loader ${BASEDIR} +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/snapshot-dependencies ${BASEDIR} +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/application ${BASEDIR} + +# Copy ESPER dependencies +COPY --chown=${EMS_USER}:${EMS_USER} jars/esper*.jar ${BASEDIR}/BOOT-INF/lib/ + +EXPOSE 2222 +EXPOSE 8111 +EXPOSE 61610 +EXPOSE 61616 +EXPOSE 61617 + +ENTRYPOINT ["dumb-init", "./bin/run.sh"] \ No newline at end of file diff --git a/ems-core/control-service/src/main/docker/Dockerfile-alpine b/ems-core/control-service/src/main/docker/Dockerfile-alpine new file mode 100644 index 0000000..56c62e9 --- /dev/null +++ b/ems-core/control-service/src/main/docker/Dockerfile-alpine @@ -0,0 +1,79 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +ARG BUILDER_IMAGE=eclipse-temurin:17.0.8_7-jre-alpine +ARG RUN_IMAGE=eclipse-temurin:17.0.8_7-jre-alpine + +# ----------------- Builder image ----------------- +FROM $BUILDER_IMAGE as ems-server-builder +#FROM vegardit/graalvm-maven:latest-java17 +WORKDIR /app +COPY jars/control-service.jar . +RUN java -Djarmode=layertools -jar control-service.jar extract + +# ----------------- Run image ----------------- +FROM $RUN_IMAGE + +# Setup environment +ENV BASEDIR /opt/ems-server +ENV EMS_HOME ${BASEDIR} +ENV EMS_CONFIG_DIR ${BASEDIR}/config + +ENV BIN_DIR ${BASEDIR}/bin +ENV CONFIG_DIR ${BASEDIR}/config +ENV LOGS_DIR ${BASEDIR}/logs +ENV PUBLIC_DIR ${BASEDIR}/public_resources + +# Install required and optional packages +RUN apk update && apk add \ + dumb-init curl bash \ + netcat-openbsd \ + vim \ + iputils-ping + +# Add an EMS user +ARG EMS_USER=emsuser +RUN mkdir ${EMS_HOME} ; \ + addgroup ${EMS_USER} ; \ + adduser --home ${EMS_HOME} --no-create-home --ingroup ${EMS_USER} --disabled-password ${EMS_USER} ; \ + chown ${EMS_USER}:${EMS_USER} ${EMS_HOME} + +USER ${EMS_USER} +WORKDIR ${BASEDIR} + +# Download a JRE suitable for running EMS clients, and +# offer it for download +ENV JRE_LINUX_PACKAGE zulu17.44.15-ca-jre17.0.8-linux_x64.tar.gz +RUN mkdir -p ${PUBLIC_DIR}/resources +RUN curl https://cdn.azul.com/zulu/bin/${JRE_LINUX_PACKAGE} --output ${PUBLIC_DIR}/resources/${JRE_LINUX_PACKAGE} + +# Copy resource files to image +ADD --chown=${EMS_USER}:${EMS_USER} bin ${BIN_DIR} +ADD --chown=${EMS_USER}:${EMS_USER} config ${CONFIG_DIR} +ADD --chown=${EMS_USER}:${EMS_USER} public_resources ${PUBLIC_DIR} + +RUN mkdir ${LOGS_DIR} +RUN chmod +rx ${BIN_DIR}/*.sh + +# Copy files from builder container +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/dependencies ${BASEDIR} +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/spring-boot-loader ${BASEDIR} +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/snapshot-dependencies ${BASEDIR} +COPY --chown=${EMS_USER}:${EMS_USER} --from=ems-server-builder /app/application ${BASEDIR} + +# Copy ESPER dependencies +COPY --chown=${EMS_USER}:${EMS_USER} jars/esper*.jar ${BASEDIR}/BOOT-INF/lib/ + +EXPOSE 2222 +EXPOSE 8111 +EXPOSE 61610 +EXPOSE 61616 +EXPOSE 61617 + +ENTRYPOINT ["dumb-init", "./bin/run.sh"] \ No newline at end of file diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/commons/NotificationResult.java b/ems-core/control-service/src/main/java/eu/melodic/models/commons/NotificationResult.java new file mode 100644 index 0000000..0f3b945 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/commons/NotificationResult.java @@ -0,0 +1,42 @@ +package eu.melodic.models.commons; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.lang.Object; +import java.lang.String; +import java.util.Map; + +@JsonDeserialize( + as = NotificationResultImpl.class +) +public interface NotificationResult { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + StatusType getStatus(); + + void setStatus(StatusType status); + + String getErrorCode(); + + void setErrorCode(String errorCode); + + String getErrorDescription(); + + void setErrorDescription(String errorDescription); + + enum StatusType { + @JsonProperty("SUCCESS") + SUCCESS("SUCCESS"), + + @JsonProperty("ERROR") + ERROR("ERROR"); + + private String name; + + StatusType(String name) { + this.name = name; + } + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/commons/NotificationResultImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/commons/NotificationResultImpl.java new file mode 100644 index 0000000..6fd5272 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/commons/NotificationResultImpl.java @@ -0,0 +1,72 @@ +package eu.melodic.models.commons; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "status", + "errorCode", + "errorDescription" +}) +public class NotificationResultImpl implements NotificationResult { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("status") + private StatusType status; + + @JsonProperty("errorCode") + private String errorCode; + + @JsonProperty("errorDescription") + private String errorDescription; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("status") + public StatusType getStatus() { + return this.status; + } + + @JsonProperty("status") + public void setStatus(StatusType status) { + this.status = status; + } + + @JsonProperty("errorCode") + public String getErrorCode() { + return this.errorCode; + } + + @JsonProperty("errorCode") + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + @JsonProperty("errorDescription") + public String getErrorDescription() { + return this.errorDescription; + } + + @JsonProperty("errorDescription") + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/commons/Watermark.java b/ems-core/control-service/src/main/java/eu/melodic/models/commons/Watermark.java new file mode 100644 index 0000000..4a69788 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/commons/Watermark.java @@ -0,0 +1,32 @@ +package eu.melodic.models.commons; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.lang.Object; +import java.lang.String; +import java.util.Date; +import java.util.Map; + +@JsonDeserialize( + as = WatermarkImpl.class +) +public interface Watermark { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + String getUser(); + + void setUser(String user); + + String getSystem(); + + void setSystem(String system); + + Date getDate(); + + void setDate(Date date); + + String getUuid(); + + void setUuid(String uuid); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/commons/WatermarkImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/commons/WatermarkImpl.java new file mode 100644 index 0000000..35e39eb --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/commons/WatermarkImpl.java @@ -0,0 +1,92 @@ +package eu.melodic.models.commons; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.lang.Object; +import java.lang.String; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "user", + "system", + "date", + "uuid" +}) +public class WatermarkImpl implements Watermark { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("user") + private String user; + + @JsonProperty("system") + private String system; + + @JsonProperty("date") + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ssZ" + ) + private Date date; + + @JsonProperty("uuid") + private String uuid; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("user") + public String getUser() { + return this.user; + } + + @JsonProperty("user") + public void setUser(String user) { + this.user = user; + } + + @JsonProperty("system") + public String getSystem() { + return this.system; + } + + @JsonProperty("system") + public void setSystem(String system) { + this.system = system; + } + + @JsonProperty("date") + public Date getDate() { + return this.date; + } + + @JsonProperty("date") + public void setDate(Date date) { + this.date = date; + } + + @JsonProperty("uuid") + public String getUuid() { + return this.uuid; + } + + @JsonProperty("uuid") + public void setUuid(String uuid) { + this.uuid = uuid; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/CamelModelRequest.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/CamelModelRequest.java new file mode 100644 index 0000000..6f4bd01 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/CamelModelRequest.java @@ -0,0 +1,29 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.Map; + +@JsonDeserialize( + as = CamelModelRequestImpl.class +) +public interface CamelModelRequest { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + String getApplicationId(); + + void setApplicationId(String applicationId); + + String getNotificationURI(); + + void setNotificationURI(String notificationURI); + + Watermark getWatermark(); + + void setWatermark(Watermark watermark); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/CamelModelRequestImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/CamelModelRequestImpl.java new file mode 100644 index 0000000..50a9ea2 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/CamelModelRequestImpl.java @@ -0,0 +1,74 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "applicationId", + "notificationURI", + "watermark" +}) +public class CamelModelRequestImpl implements CamelModelRequest { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("applicationId") + private String applicationId; + + @JsonProperty("notificationURI") + private String notificationURI; + + @JsonProperty("watermark") + private Watermark watermark; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("applicationId") + public String getApplicationId() { + return this.applicationId; + } + + @JsonProperty("applicationId") + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + @JsonProperty("notificationURI") + public String getNotificationURI() { + return this.notificationURI; + } + + @JsonProperty("notificationURI") + public void setNotificationURI(String notificationURI) { + this.notificationURI = notificationURI; + } + + @JsonProperty("watermark") + public Watermark getWatermark() { + return this.watermark; + } + + @JsonProperty("watermark") + public void setWatermark(Watermark watermark) { + this.watermark = watermark; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Interval.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Interval.java new file mode 100644 index 0000000..97e376e --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Interval.java @@ -0,0 +1,53 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.lang.Object; +import java.lang.String; +import java.util.Map; + +@JsonDeserialize( + as = IntervalImpl.class +) +public interface Interval { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + UnitType getUnit(); + + void setUnit(UnitType unit); + + int getPeriod(); + + void setPeriod(int period); + + enum UnitType { + @JsonProperty("DAYS") + DAYS("DAYS"), + + @JsonProperty("HOURS") + HOURS("HOURS"), + + @JsonProperty("MICROSECONDS") + MICROSECONDS("MICROSECONDS"), + + @JsonProperty("MILLISECONDS") + MILLISECONDS("MILLISECONDS"), + + @JsonProperty("MINUTES") + MINUTES("MINUTES"), + + @JsonProperty("NANOSECONDS") + NANOSECONDS("NANOSECONDS"), + + @JsonProperty("SECONDS") + SECONDS("SECONDS"); + + private String name; + + UnitType(String name) { + this.name = name; + } + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/IntervalImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/IntervalImpl.java new file mode 100644 index 0000000..ef4aceb --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/IntervalImpl.java @@ -0,0 +1,58 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "unit", + "period" +}) +public class IntervalImpl implements Interval { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("unit") + private UnitType unit; + + @JsonProperty("period") + private int period; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("unit") + public UnitType getUnit() { + return this.unit; + } + + @JsonProperty("unit") + public void setUnit(UnitType unit) { + this.unit = unit; + } + + @JsonProperty("period") + public int getPeriod() { + return this.period; + } + + @JsonProperty("period") + public void setPeriod(int period) { + this.period = period; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/KeyValuePair.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/KeyValuePair.java new file mode 100644 index 0000000..d6cab79 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/KeyValuePair.java @@ -0,0 +1,23 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.lang.Object; +import java.lang.String; +import java.util.Map; + +@JsonDeserialize( + as = KeyValuePairImpl.class +) +public interface KeyValuePair { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + String getKey(); + + void setKey(String key); + + String getValue(); + + void setValue(String value); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/KeyValuePairImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/KeyValuePairImpl.java new file mode 100644 index 0000000..2f88607 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/KeyValuePairImpl.java @@ -0,0 +1,58 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "key", + "value" +}) +public class KeyValuePairImpl implements KeyValuePair { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("key") + private String key; + + @JsonProperty("value") + private String value; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("key") + public String getKey() { + return this.key; + } + + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + @JsonProperty("value") + public String getValue() { + return this.value; + } + + @JsonProperty("value") + public void setValue(String value) { + this.value = value; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Monitor.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Monitor.java new file mode 100644 index 0000000..cf07aee --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Monitor.java @@ -0,0 +1,37 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.lang.Object; +import java.lang.String; +import java.util.List; +import java.util.Map; + +@JsonDeserialize( + as = MonitorImpl.class +) +public interface Monitor { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + String getMetric(); + + void setMetric(String metric); + + String getComponent(); + + void setComponent(String component); + + Sensor getSensor(); + + void setSensor(Sensor sensor); + + List getSinks(); + + void setSinks(List sinks); + + List getTags(); + + void setTags(List tags); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorImpl.java new file mode 100644 index 0000000..405ab8f --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorImpl.java @@ -0,0 +1,102 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "metric", + "component", + "sensor", + "sinks", + "tags" +}) +public class MonitorImpl implements Monitor { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("metric") + private String metric; + + @JsonProperty("component") + private String component; + + @JsonProperty("sensor") + private Sensor sensor; + + @JsonProperty("sinks") + private List sinks; + + @JsonProperty("tags") + private List tags; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("metric") + public String getMetric() { + return this.metric; + } + + @JsonProperty("metric") + public void setMetric(String metric) { + this.metric = metric; + } + + @JsonProperty("component") + public String getComponent() { + return this.component; + } + + @JsonProperty("component") + public void setComponent(String component) { + this.component = component; + } + + @JsonProperty("sensor") + public Sensor getSensor() { + return this.sensor; + } + + @JsonProperty("sensor") + public void setSensor(Sensor sensor) { + this.sensor = sensor; + } + + @JsonProperty("sinks") + public List getSinks() { + return this.sinks; + } + + @JsonProperty("sinks") + public void setSinks(List sinks) { + this.sinks = sinks; + } + + @JsonProperty("tags") + public List getTags() { + return this.tags; + } + + @JsonProperty("tags") + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataRequest.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataRequest.java new file mode 100644 index 0000000..9ef094d --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataRequest.java @@ -0,0 +1,25 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.Map; + +@JsonDeserialize( + as = MonitorsDataRequestImpl.class +) +public interface MonitorsDataRequest { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + String getApplicationId(); + + void setApplicationId(String applicationId); + + Watermark getWatermark(); + + void setWatermark(Watermark watermark); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataRequestImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataRequestImpl.java new file mode 100644 index 0000000..522e346 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataRequestImpl.java @@ -0,0 +1,60 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "applicationId", + "watermark" +}) +public class MonitorsDataRequestImpl implements MonitorsDataRequest { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("applicationId") + private String applicationId; + + @JsonProperty("watermark") + private Watermark watermark; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("applicationId") + public String getApplicationId() { + return this.applicationId; + } + + @JsonProperty("applicationId") + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + @JsonProperty("watermark") + public Watermark getWatermark() { + return this.watermark; + } + + @JsonProperty("watermark") + public void setWatermark(Watermark watermark) { + this.watermark = watermark; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataResponse.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataResponse.java new file mode 100644 index 0000000..1b874f6 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataResponse.java @@ -0,0 +1,26 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.List; +import java.util.Map; + +@JsonDeserialize( + as = MonitorsDataResponseImpl.class +) +public interface MonitorsDataResponse { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + List getMonitors(); + + void setMonitors(List monitors); + + Watermark getWatermark(); + + void setWatermark(Watermark watermark); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataResponseImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataResponseImpl.java new file mode 100644 index 0000000..b03b76a --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/MonitorsDataResponseImpl.java @@ -0,0 +1,61 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "monitors", + "watermark" +}) +public class MonitorsDataResponseImpl implements MonitorsDataResponse { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("monitors") + private List monitors; + + @JsonProperty("watermark") + private Watermark watermark; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("monitors") + public List getMonitors() { + return this.monitors; + } + + @JsonProperty("monitors") + public void setMonitors(List monitors) { + this.monitors = monitors; + } + + @JsonProperty("watermark") + public Watermark getWatermark() { + return this.watermark; + } + + @JsonProperty("watermark") + public void setWatermark(Watermark watermark) { + this.watermark = watermark; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PullSensor.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PullSensor.java new file mode 100644 index 0000000..addaac7 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PullSensor.java @@ -0,0 +1,29 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.lang.Object; +import java.lang.String; +import java.util.List; +import java.util.Map; + +@JsonDeserialize( + as = PullSensorImpl.class +) +public interface PullSensor { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + String getClassName(); + + void setClassName(String className); + + List getConfiguration(); + + void setConfiguration(List configuration); + + Interval getInterval(); + + void setInterval(Interval interval); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PullSensorImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PullSensorImpl.java new file mode 100644 index 0000000..edaf90b --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PullSensorImpl.java @@ -0,0 +1,73 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "className", + "configuration", + "interval" +}) +public class PullSensorImpl implements PullSensor { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("className") + private String className; + + @JsonProperty("configuration") + private List configuration; + + @JsonProperty("interval") + private Interval interval; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("className") + public String getClassName() { + return this.className; + } + + @JsonProperty("className") + public void setClassName(String className) { + this.className = className; + } + + @JsonProperty("configuration") + public List getConfiguration() { + return this.configuration; + } + + @JsonProperty("configuration") + public void setConfiguration(List configuration) { + this.configuration = configuration; + } + + @JsonProperty("interval") + public Interval getInterval() { + return this.interval; + } + + @JsonProperty("interval") + public void setInterval(Interval interval) { + this.interval = interval; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PushSensor.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PushSensor.java new file mode 100644 index 0000000..909d324 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PushSensor.java @@ -0,0 +1,19 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.lang.Object; +import java.lang.String; +import java.util.Map; + +@JsonDeserialize( + as = PushSensorImpl.class +) +public interface PushSensor { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + int getPort(); + + void setPort(int port); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PushSensorImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PushSensorImpl.java new file mode 100644 index 0000000..2c08ff7 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/PushSensorImpl.java @@ -0,0 +1,42 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder("port") +public class PushSensorImpl implements PushSensor { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("port") + private int port; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("port") + public int getPort() { + return this.port; + } + + @JsonProperty("port") + public void setPort(int port) { + this.port = port; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Sensor.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Sensor.java new file mode 100644 index 0000000..7eb8ec5 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Sensor.java @@ -0,0 +1,49 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import eu.melodic.models.resources.SensorDeserializer; +import eu.melodic.models.resources.SensorSerializer; + +import java.lang.IllegalStateException; +import java.lang.Object; + +@JsonDeserialize( + using = SensorDeserializer.class +) +@JsonSerialize( + using = SensorSerializer.class +) +public class Sensor { + private Object anyType; + + private Sensor() { + this.anyType = null; + } + + public Sensor(PushSensor pushSensor) { + this.anyType = pushSensor; + } + + public Sensor(PullSensor pullSensor) { + this.anyType = pullSensor; + } + + public PushSensor getPushSensor() { + if ( !(anyType instanceof PushSensor)) throw new IllegalStateException("fetching wrong type out of the union: PushSensor"); + return (PushSensor) anyType; + } + + public boolean isPushSensor() { + return anyType instanceof PushSensor; + } + + public PullSensor getPullSensor() { + if ( !(anyType instanceof PullSensor)) throw new IllegalStateException("fetching wrong type out of the union: PullSensor"); + return (PullSensor) anyType; + } + + public boolean isPullSensor() { + return anyType instanceof PullSensor; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Sink.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Sink.java new file mode 100644 index 0000000..8ff0dba --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/Sink.java @@ -0,0 +1,46 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.lang.Object; +import java.lang.String; +import java.util.List; +import java.util.Map; + +@JsonDeserialize( + as = SinkImpl.class +) +public interface Sink { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + TypeType getType(); + + void setType(TypeType type); + + List getConfiguration(); + + void setConfiguration(List configuration); + + enum TypeType { + @JsonProperty("KAIROS_DB") + KAIROSDB("KAIROS_DB"), + + @JsonProperty("INFLUX") + INFLUX("INFLUX"), + + @JsonProperty("JMS") + JMS("JMS"), + + @JsonProperty("CLI") + CLI("CLI"); + + private String name; + + TypeType(String name) { + this.name = name; + } + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/SinkImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/SinkImpl.java new file mode 100644 index 0000000..092b1f3 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/interfaces/SinkImpl.java @@ -0,0 +1,60 @@ +package eu.melodic.models.interfaces; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "type", + "configuration" +}) +public class SinkImpl implements Sink { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("type") + private TypeType type; + + @JsonProperty("configuration") + private List configuration; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("type") + public TypeType getType() { + return this.type; + } + + @JsonProperty("type") + public void setType(TypeType type) { + this.type = type; + } + + @JsonProperty("configuration") + public List getConfiguration() { + return this.configuration; + } + + @JsonProperty("configuration") + public void setConfiguration(List configuration) { + this.configuration = configuration; + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/resources/SensorDeserializer.java b/ems-core/control-service/src/main/java/eu/melodic/models/resources/SensorDeserializer.java new file mode 100644 index 0000000..c6ad6db --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/resources/SensorDeserializer.java @@ -0,0 +1,36 @@ +package eu.melodic.models.resources; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import gr.iccs.imu.ems.util.StrUtil; +import eu.melodic.models.interfaces.PullSensor; +import eu.melodic.models.interfaces.PushSensor; +import eu.melodic.models.interfaces.Sensor; +import java.io.IOException; +import java.lang.Object; +import java.lang.String; +import java.util.Arrays; +import java.util.Map; + +public class SensorDeserializer extends StdDeserializer { + public SensorDeserializer() { + super(Sensor.class);} + + private boolean looksLikePushSensor(Map map) { + return map.keySet().containsAll(Arrays.asList("port")); + } + + private boolean looksLikePullSensor(Map map) { + return map.keySet().containsAll(Arrays.asList("className","configuration","interval")); + } + + public Sensor deserialize(JsonParser jsonParser, DeserializationContext jsonContext) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + Map map = StrUtil.castToMapStringObject( mapper.readValue(jsonParser, Map.class) ); + if ( looksLikePushSensor(map) ) return new Sensor(mapper.convertValue(map, PushSensor.class)); + if ( looksLikePullSensor(map) ) return new Sensor(mapper.convertValue(map, PullSensor.class)); + throw new IOException("Can't figure out type of object" + map); + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/resources/SensorSerializer.java b/ems-core/control-service/src/main/java/eu/melodic/models/resources/SensorSerializer.java new file mode 100644 index 0000000..a5c8e18 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/resources/SensorSerializer.java @@ -0,0 +1,25 @@ +package eu.melodic.models.resources; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import eu.melodic.models.interfaces.Sensor; +import java.io.IOException; + +public class SensorSerializer extends StdSerializer { + public SensorSerializer() { + super(Sensor.class);} + + public void serialize(Sensor object, JsonGenerator jsonGenerator, SerializerProvider jsonSerializerProvider) throws IOException, JsonProcessingException { + if ( object.isPushSensor()) { + jsonGenerator.writeObject(object.getPushSensor()); + return; + } + if ( object.isPullSensor()) { + jsonGenerator.writeObject(object.getPullSensor()); + return; + } + throw new IOException("Can't figure out type of object" + object); + } +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/services/CamelModelNotificationRequest.java b/ems-core/control-service/src/main/java/eu/melodic/models/services/CamelModelNotificationRequest.java new file mode 100644 index 0000000..25c35d7 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/services/CamelModelNotificationRequest.java @@ -0,0 +1,30 @@ +package eu.melodic.models.services; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import eu.melodic.models.commons.NotificationResult; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.Map; + +@JsonDeserialize( + as = CamelModelNotificationRequestImpl.class +) +public interface CamelModelNotificationRequest { + Map getAdditionalProperties(); + + void setAdditionalProperties(Map additionalProperties); + + String getApplicationId(); + + void setApplicationId(String applicationId); + + NotificationResult getResult(); + + void setResult(NotificationResult result); + + Watermark getWatermark(); + + void setWatermark(Watermark watermark); +} diff --git a/ems-core/control-service/src/main/java/eu/melodic/models/services/CamelModelNotificationRequestImpl.java b/ems-core/control-service/src/main/java/eu/melodic/models/services/CamelModelNotificationRequestImpl.java new file mode 100644 index 0000000..fdb0b36 --- /dev/null +++ b/ems-core/control-service/src/main/java/eu/melodic/models/services/CamelModelNotificationRequestImpl.java @@ -0,0 +1,75 @@ +package eu.melodic.models.services; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import eu.melodic.models.commons.NotificationResult; +import eu.melodic.models.commons.Watermark; + +import java.lang.Object; +import java.lang.String; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "applicationId", + "result", + "watermark" +}) +public class CamelModelNotificationRequestImpl implements CamelModelNotificationRequest { + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("applicationId") + private String applicationId; + + @JsonProperty("result") + private NotificationResult result; + + @JsonProperty("watermark") + private Watermark watermark; + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @JsonProperty("applicationId") + public String getApplicationId() { + return this.applicationId; + } + + @JsonProperty("applicationId") + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + @JsonProperty("result") + public NotificationResult getResult() { + return this.result; + } + + @JsonProperty("result") + public void setResult(NotificationResult result) { + this.result = result; + } + + @JsonProperty("watermark") + public Watermark getWatermark() { + return this.watermark; + } + + @JsonProperty("watermark") + public void setWatermark(Watermark watermark) { + this.watermark = watermark; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/ApplicationContext.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/ApplicationContext.java new file mode 100644 index 0000000..b403e89 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/ApplicationContext.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control; + +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import gr.iccs.imu.ems.control.util.WebClientUtil; +import gr.iccs.imu.ems.util.EventBus; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class ApplicationContext { + private final ControlServiceProperties properties; + + @Bean + @SneakyThrows + @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) + public WebClient webClient() { + return new WebClientUtil().createInstance(properties.getSsl()); + } + + @Bean + @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) + public EventBus eventBus() { + return EventBus.builder().build(); + } + + @Bean + @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setDaemon(true); + log.debug("ApplicationContext: taskScheduler: NEW INSTANCE CREATED: {}", taskExecutor); + return taskExecutor; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/ControlServiceApplication.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/ControlServiceApplication.java new file mode 100644 index 0000000..a3600b8 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/ControlServiceApplication.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control; + +import com.ulisesbocchio.jasyptspringboot.environment.StandardEncryptableEnvironment; +import gr.iccs.imu.ems.control.controller.ControlServiceCoordinator; +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import gr.iccs.imu.ems.util.EventBus; +import gr.iccs.imu.ems.util.KeystoreUtil; +import gr.iccs.imu.ems.util.PasswordUtil; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.connector.Connector; +import org.springframework.boot.Banner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.ApplicationPidFileWriter; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +@Slf4j +@EnableAsync +@EnableScheduling +@Configuration +@SpringBootApplication( + scanBasePackages = { + "gr.iccs.imu.ems.baguette.server", "gr.iccs.imu.ems.baguette.client.install", + "gr.iccs.imu.ems.baguette.client.selfhealing", "gr.iccs.imu.ems.brokercep", + "gr.iccs.imu.ems.control", "gr.iccs.imu.ems.translate", + "gr.iccs.imu.ems.common", "gr.iccs.imu.ems.util", + "gr.iccs.imu.ems.brokerclient", + "${scan.packages}" + }, + exclude = { SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class } ) +@RequiredArgsConstructor +public class ControlServiceApplication { + private static ConfigurableApplicationContext applicationContext; + private static Timer exitTimer; + + private final ControlServiceProperties properties; + private final PasswordUtil passwordUtil; + + public static void main(String[] args) { + long initStartTime = System.currentTimeMillis(); + + // Start EMS server + SpringApplication springApplication = new SpringApplicationBuilder() + .environment(new StandardEncryptableEnvironment()) + .sources(ControlServiceApplication.class) + .build(); + //SpringApplication springApplication = new SpringApplication(ControlServiceApplication.class); + springApplication.setBannerMode(Banner.Mode.LOG); + springApplication.addListeners(new ApplicationPidFileWriter("./ems.pid")); + applicationContext = springApplication.run(args); + + // Load configured plugins + /*BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner( + beanFactory, true, applicationContext.getEnvironment()); + scanner.scan("gr.iccs.imu.ems"); + */ + + long initEndTime = System.currentTimeMillis(); + log.info("EMS server initialized in {}ms", initEndTime-initStartTime); + StrUtil.castToEventBusStringObjectObject(applicationContext.getBean(EventBus.class)) + .send(ControlServiceCoordinator.COORDINATOR_STATUS_TOPIC, Map.of( + "state", "EMS STARTED", + "message", "EMS server initialized in "+(initEndTime-initStartTime)+"ms", + "timestamp", System.currentTimeMillis() + ), applicationContext.getBean(ControlServiceApplication.class)); + } + + @Bean + public ServletWebServerFactory servletWebServerFactory() { + return new TomcatServletWebServerFactory() { + protected void customizeConnector(Connector connector) { + if (this.getSsl() != null && this.getSsl().isEnabled()) { + try { + log.debug("TomcatServletWebServerFactory: ControlServiceProperties: {}", properties); + log.debug("TomcatServletWebServerFactory: Keystore password: {}", passwordUtil.encodePassword(properties.getSsl().getKeystorePassword())); + log.debug("TomcatServletWebServerFactory: Truststore password: {}", passwordUtil.encodePassword(properties.getSsl().getTruststorePassword())); + + log.debug("TomcatServletWebServerFactory: Initializing HTTPS keystore, truststore and certificate..."); + KeystoreUtil.initializeKeystoresAndCertificate(properties.getSsl(), passwordUtil); + log.debug("TomcatServletWebServerFactory: Initializing HTTPS keystore, truststore and certificate... done"); + } catch (Exception e) { + log.error("TomcatServletWebServerFactory: EXCEPTION while initializing HTTPS keystore, truststore and certificate:\n", e); + } + } + super.customizeConnector(connector); + } + }; + } + + public synchronized static void exitApp(int exitCode, long gracePeriod) { + if (exitTimer==null) { + // Wait for 'gracePeriod' seconds before forcing JVM to exit + log.info("ControlServiceApplication.exitApp(): Wait for {}sec before exit", gracePeriod); + exitTimer = new Timer("exit-timer", true); + exitTimer.schedule(new TimerTask() { + @Override + public void run() { + log.info("ControlServiceApplication.exitApp(): exit-timer: Exiting with code: {}", exitCode); + System.exit(exitCode); + log.info("ControlServiceApplication.exitApp(): exit-timer: Bye"); + } + }, gracePeriod * 1000); + + // Close SpringBoot application + log.info("ControlServiceApplication.exitApp(): Closing application context..."); + ExitCodeGenerator exitCodeGenerator = () -> { + log.info("ControlServiceApplication.exitApp(): exitCodeGenerator: Exit code: {}", exitCode); + return exitCode; + }; + SpringApplication.exit(applicationContext, exitCodeGenerator); + log.info("ControlServiceApplication.exitApp(): Exiting with code: {}", exitCode); + System.exit(exitCode); + + } else { + log.warn("ControlServiceApplication.exitApp(): Exit timer has already started: {}", exitTimer); + } + } +} \ No newline at end of file diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/Collector.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/Collector.java new file mode 100644 index 0000000..653c21f --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/Collector.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.collector; + +import gr.iccs.imu.ems.util.Plugin; + +public interface Collector extends Plugin { +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/ServerCollectorContext.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/ServerCollectorContext.java new file mode 100644 index 0000000..bef3049 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/ServerCollectorContext.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.collector; + +import gr.iccs.imu.ems.baguette.server.NodeRegistry; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.util.ClientConfiguration; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ServerCollectorContext implements CollectorContext { + private final NodeRegistry nodeRegistry; + private final BrokerCepService brokerCepService; + + @Override + public List getNodeConfigurations() { + return null; + } + + @Override + public Set getNodesWithoutClient() { + if (nodeRegistry==null || nodeRegistry.getCoordinator()==null) return null; + return nodeRegistry.getCoordinator().supportsAggregators() + ? Collections.emptySet() + : nodeRegistry.getNodes().stream() + .filter(entry -> entry.getState()== NodeRegistryEntry.STATE.NOT_INSTALLED) + .map(NodeRegistryEntry::getIpAddress) + .collect(Collectors.toCollection(HashSet::new)); + } + + @Override + public boolean isAggregator() { + return true; + } + + @Override + @SneakyThrows + public PUBLISH_RESULT sendEvent(String connectionString, String destinationName, EventMap event, boolean createDestination) { + assert(connectionString==null); + if (createDestination || brokerCepService.destinationExists(destinationName)) { + brokerCepService.publishEvent(null, destinationName, event); + return PUBLISH_RESULT.SENT; + } + return PUBLISH_RESULT.SKIPPED; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/netdata/ServerNetdataCollector.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/netdata/ServerNetdataCollector.java new file mode 100644 index 0000000..b37df76 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/collector/netdata/ServerNetdataCollector.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.collector.netdata; + +import gr.iccs.imu.ems.common.collector.CollectorContext; +import gr.iccs.imu.ems.common.collector.netdata.NetdataCollectorProperties; +import gr.iccs.imu.ems.common.collector.netdata.NetdataCollector; +import gr.iccs.imu.ems.control.collector.Collector; +import gr.iccs.imu.ems.control.collector.ServerCollectorContext; +import gr.iccs.imu.ems.util.EventBus; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +/** + * Collects measurements from Netdata http server + */ +@Slf4j +@Component +public class ServerNetdataCollector extends NetdataCollector implements Collector { + public ServerNetdataCollector(@NonNull NetdataCollectorProperties properties, + @NonNull CollectorContext collectorContext, + @NonNull TaskScheduler taskScheduler, + @NonNull EventBus eventBus) + { + super("ServerNetdataCollector", properties, collectorContext, taskScheduler, eventBus); + if (!(collectorContext instanceof ServerCollectorContext)) + throw new IllegalArgumentException("Invalid CollectorContext provided. Expected: ServerCollectorContext, but got "+collectorContext.getClass().getName()); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/BrokerCepController.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/BrokerCepController.java new file mode 100644 index 0000000..ae7e075 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/BrokerCepController.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import gr.iccs.imu.ems.brokercep.EventCache; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.List; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class BrokerCepController { + + private final EventCache eventCache; + + @RequestMapping(value = { "/brokercep/last-events/{howmany}", "/brokercep/last-events" }, method=GET) + public Collection getLastEvents(@PathVariable(required = false) Integer howmany) { + log.info("BrokerCepController.getLastEvents(): howmany={}", howmany); + + List cache = eventCache.asList(); + return howmany!=null && howmany >0 && howmany controllerEndpoints; + @Getter + private List controllerEndpointsShort; + + // ------------------------------------------------------------------------------------------------------------ + // ESB and Upperware interfacing methods + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/camelModel", method = POST) + public String newAppModel(@RequestBody CamelModelRequestImpl request, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.debug("ControlServiceController.newAppModel(): Received request: {}", request); + log.trace("ControlServiceController.newAppModel(): JWT token: {}", jwtToken); + + // Get information from request + String applicationId = request.getApplicationId(); + String notificationUri = request.getNotificationURI(); + String requestUuid = request.getWatermark().getUuid(); + log.info("ControlServiceController.newAppModel(): Request info: app-id={}, notification-uri={}, request-id={}", + applicationId, notificationUri, requestUuid); + + // Check parameters + if (StringUtils.isBlank(applicationId)) { + log.warn("ControlServiceController.newAppModel(): Request does not contain an application id"); + throw new RestControllerException(400, "Request does not contain an application id"); + } + + // Start translation and reconfiguration in a worker thread + coordinator.processAppModel(applicationId, null, ControlServiceRequestInfo.create(notificationUri, requestUuid, jwtToken)); + log.debug("ControlServiceController.newAppModel()/camelModel: Model translation dispatched to a worker thread"); + + return "OK"; + } + + @RequestMapping(value = "/appModelJson", method = POST, + consumes = MediaType.APPLICATION_JSON_VALUE) + public String newAppModel(@RequestBody String requestStr, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.debug("ControlServiceController.newAppModel(): Received request: {}", requestStr); + log.trace("ControlServiceController.newAppModel()/camelModelJson: JWT token: {}", jwtToken); + + // Use Gson to get model id's from request body (in JSON format) + com.google.gson.JsonObject jobj = new com.google.gson.Gson().fromJson(requestStr, com.google.gson.JsonObject.class); + String appModelId = Optional.ofNullable(jobj.get("app-model-id")).map(je -> stripQuotes(je.toString())).orElse(null); + String cpModelId = Optional.ofNullable(jobj.get("cp-model-id")).map(je -> stripQuotes(je.toString())).orElse(null); + log.info("ControlServiceController.newAppModel(): App model id from request: {}", appModelId); + log.info("ControlServiceController.newAppModel(): CP model id from request: {}", cpModelId); + + // Check parameters + if (StringUtils.isBlank(appModelId)) { + log.warn("ControlServiceController.newAppModel(): Request does not contain an app. model id"); + throw new RestControllerException(400, "Request does not contain an application id"); + } + + // Start translation and component reconfiguration in a worker thread + coordinator.processAppModel(appModelId, cpModelId, ControlServiceRequestInfo.create(null, null, jwtToken)); + log.debug("ControlServiceController.newAppModel(): Model translation dispatched to a worker thread"); + + return "OK"; + } + + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/cpModelJson", method = POST, + consumes = MediaType.APPLICATION_JSON_VALUE) + public String newCpModel(@RequestBody String requestStr, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.debug("ControlServiceController.newCpModel(): Received request: {}", requestStr); + log.trace("ControlServiceController.newCpModel(): JWT token: {}", jwtToken); + + // Use Gson to get model id's from request body (in JSON format) + com.google.gson.JsonObject jobj = new com.google.gson.Gson().fromJson(requestStr, com.google.gson.JsonObject.class); + String cpModelId = Optional.ofNullable(jobj.get("cp-model-id")).map(je -> stripQuotes(je.toString())).orElse(null); + log.info("ControlServiceController.newCpModel(): CP model id from request: {}", cpModelId); + + // Check parameters + if (StringUtils.isBlank(cpModelId)) { + log.warn("ControlServiceController.newCpModel(): Request does not contain a CP model id"); + throw new RestControllerException(400, "Request does not contain a CP model id"); + } + + // Start CP model processing in a worker thread + coordinator.processCpModel(cpModelId, ControlServiceRequestInfo.create(null, null, jwtToken)); + log.debug("ControlServiceController.newCpModel(): CP Model processing dispatched to a worker thread"); + + return "OK"; + } + + @RequestMapping(value = "/cpConstants", method = POST, + consumes = MediaType.APPLICATION_JSON_VALUE) + public String setConstants(@RequestBody String requestStr, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.debug("ControlServiceController.setConstants(): Received request: {}", requestStr); + log.trace("ControlServiceController.setConstants(): JWT token: {}", jwtToken); + + // Use Gson to get constants from request body (in JSON format) + Type type = new TypeToken>(){}.getType(); + Map constants = new com.google.gson.Gson().fromJson(requestStr, type); + log.info("ControlServiceController.setConstants(): Constants from request: {}", constants); + + // Start CP model processing in a worker thread + coordinator.setConstants(constants, ControlServiceRequestInfo.create(null, null, jwtToken)); + log.debug("ControlServiceController.setConstants(): Constants set"); + + return "OK"; + } + + /*@RequestMapping(value = "/test/**", method = {GET, POST}) + public String test(HttpServletRequest request, @RequestBody(required = false) String body, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + String path = request.getRequestURI().split("/test/", 2)[1]; + Map headers = Collections.list(request.getHeaderNames()).stream() + .collect(Collectors.toMap(h -> h, request::getHeader)); + log.warn("-------------- TEST endpoint: --------------------------------------------------------"); + log.warn("-------------- TEST endpoint: Verb/URL: {} {}", request.getMethod(), UriUtils.decode(path, StandardCharsets.UTF_8)); + log.warn("-------------- TEST endpoint: headers: {}", headers); + log.warn("-------------- TEST endpoint: body: {}", body); + log.warn("-------------- TEST endpoint: JWT: {}", jwtToken); + return "OK"; + }*/ + + // --------------------------------------------------------------------------------------------------- + // Translator results methods + // --------------------------------------------------------------------------------------------------- + + @RequestMapping(value = "/translator/currentAppModel", method = {GET,POST}) + public String getCurrentAppModel(@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.debug("ControlServiceController.getCurrentAppModel(): Received request"); + log.trace("ControlServiceController.getCurrentAppModel(): JWT token: {}", jwtToken); + + String currentAppModelId = coordinator.getCurrentAppModelId(); + log.info("ControlServiceController.getCurrentAppModel(): Current App model: {}", currentAppModelId); + + return currentAppModelId; + } + + @RequestMapping(value = "/translator/currentCpModel", method = {GET,POST}) + public String getCurrentCpModel(@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.debug("ControlServiceController.getCurrentCpModel(): Received request"); + log.trace("ControlServiceController.getCurrentCpModel(): JWT token: {}", jwtToken); + + String currentCpModelId = coordinator.getCurrentCpModelId(); + log.info("ControlServiceController.getCurrentCpModel(): Current CP model: {}", currentCpModelId); + + return currentCpModelId; + } + + // --------------------------------------------------------------------------------------------------- + // Helper methods + // --------------------------------------------------------------------------------------------------- + + protected String stripQuotes(String s) { + return (s != null && s.startsWith("\"") && s.endsWith("\"")) ? s.substring(1, s.length() - 1) : s; + } + + @EventListener + public void handleContextRefresh(ContextRefreshedEvent event) { + ApplicationContext applicationContext = event.getApplicationContext(); + RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext + .getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class); + Map map = requestMappingHandlerMapping + .getHandlerMethods(); + //map.forEach((key, value) -> log.info("..... {} {}", key, value)); + + controllerEndpoints = map.keySet().stream() + .filter(Objects::nonNull) + .map(RequestMappingInfo::getPatternValues) + .flatMap(Set::stream) + .collect(Collectors.toList()); + log.debug("ControlServiceController.handleContextRefresh: controller-endpoints: {}", controllerEndpoints); + + controllerEndpointsShort = controllerEndpoints.stream() + .map(s -> s.startsWith("/") ? s.substring(1) : s) + .map(s -> s.indexOf("/") > 0 ? s.split("/", 2)[0] + "/**" : s) + .map(e -> "/" + e.replaceAll("\\{.*", "**")) + .distinct() + .collect(Collectors.toList()); + log.debug("ControlServiceController.handleContextRefresh: controller-endpoints-short: {}", controllerEndpointsShort); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ControlServiceCoordinator.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ControlServiceCoordinator.java new file mode 100644 index 0000000..de5fa52 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ControlServiceCoordinator.java @@ -0,0 +1,903 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import com.google.gson.GsonBuilder; +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.NodeRegistry; +import gr.iccs.imu.ems.baguette.server.ServerCoordinator; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.BrokerCepStatementSubscriber; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.control.collector.netdata.ServerNetdataCollector; +import gr.iccs.imu.ems.control.plugin.PostTranslationPlugin; +import gr.iccs.imu.ems.control.plugin.TranslationContextPlugin; +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import gr.iccs.imu.ems.control.util.TopicBeacon; +import gr.iccs.imu.ems.control.util.TranslationContextMonitorGsonDeserializer; +import gr.iccs.imu.ems.control.util.mvv.NoopMetricVariableValuesServiceImpl; +import gr.iccs.imu.ems.util.EventBus; +import eu.melodic.models.commons.NotificationResult; +import eu.melodic.models.commons.NotificationResultImpl; +import eu.melodic.models.commons.Watermark; +import eu.melodic.models.commons.WatermarkImpl; +import eu.melodic.models.services.CamelModelNotificationRequest; +import eu.melodic.models.services.CamelModelNotificationRequestImpl; +import gr.iccs.imu.ems.translate.NoopTranslator; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.translate.TranslationContextPrinter; +import gr.iccs.imu.ems.translate.Translator; +import gr.iccs.imu.ems.translate.dag.DAGNode; +import gr.iccs.imu.ems.translate.model.Monitor; +import gr.iccs.imu.ems.translate.model.Sink; +import gr.iccs.imu.ems.translate.mvv.MetricVariableValuesService; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.EventListener; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ControlServiceCoordinator implements InitializingBean { + + public final static String COORDINATOR_STATUS_TOPIC = "COORDINATOR_STATUS_TOPIC"; + + private final ApplicationContext applicationContext; + private final ControlServiceProperties properties; + @Getter private final BaguetteServer baguetteServer; + private final NodeRegistry nodeRegistry; + private final WebClient webClient; + private final PasswordUtil passwordUtil; + private final EventBus eventBus; + + private final List translatorImplementations; + private Translator translator; // Will be populated in 'afterPropertiesSet()' + private final List postTranslationPlugins; + private final List translationContextPlugins; + private final TranslationContextPrinter translationContextPrinter; + + private final List mvvServiceImplementations; + private MetricVariableValuesService mvvService; // Will be populated in 'afterPropertiesSet()' + + @Getter private BrokerCepService brokerCep; + + private final AtomicBoolean inUse = new AtomicBoolean(); + private final Map appModelToTcCache = new HashMap<>(); + + @Getter private String currentAppModelId; + @Getter private String currentCpModelId; + private TranslationContext currentTC; + + private ServerNetdataCollector netdataCollector; + + public enum EMS_STATE { + IDLE, INITIALIZING, RECONFIGURING, READY, ERROR + } + + @Getter private EMS_STATE currentEmsState = EMS_STATE.IDLE; + @Getter private String currentEmsStateMessage; + @Getter private long currentEmsStateChangeTimestamp; + + + @Override + public void afterPropertiesSet() throws Exception { + initTranslator(); + initMvvService(); + + // Run configuration checks and throw exceptions early (before actually using EMS) + if (properties.isSkipTranslation()) { + if (StringUtils.isBlank(properties.getTcLoadFile())) + throw new IllegalArgumentException("Model translation will be skipped (see property control.skip-translation), but no Translation Context file or pattern has been set. Check property: control.tc-load-file"); + log.warn("Model translation will be skipped, and a Translation Context file will be used: tc-file-pattern={}", properties.getTcLoadFile()); + } + + log.debug("ControlServiceCoordinator.afterPropertiesSet(): Post-translation plugins: {}", postTranslationPlugins); + log.debug("ControlServiceCoordinator.afterPropertiesSet(): TranslationContext plugins: {}", translationContextPlugins); + } + + private void initMvvService() { + // Initialize MVV service + log.debug("ControlServiceCoordinator.initMvvService(): MVV service implementations: {}", mvvServiceImplementations); + if (mvvServiceImplementations.size() == 1) { + mvvService = mvvServiceImplementations.get(0); + } else if (mvvServiceImplementations.isEmpty()) { + throw new IllegalArgumentException("No MVV service implementation found"); + } else { + mvvService = mvvServiceImplementations.stream() + .filter(s -> s!=null && !(s instanceof NoopMetricVariableValuesServiceImpl)) + .findAny() + .orElse(new NoopMetricVariableValuesServiceImpl()); + } + log.debug("ControlServiceCoordinator.initMvvService(): MVV service implementation selected: {}", mvvService); + mvvService.init(); + log.debug("ControlServiceCoordinator.initMvvService(): MVV service initialized"); + } + + private void initTranslator() { + log.debug("ControlServiceCoordinator.initTranslator(): Translator implementations: {}", translatorImplementations); + if (translatorImplementations.size() == 1) { + translator = translatorImplementations.get(0); + } else if (translatorImplementations.isEmpty()) { + throw new IllegalArgumentException("No Translator implementations found"); + } else { + translator = translatorImplementations.stream() + .filter(s -> s!=null && !(s instanceof NoopTranslator)) + .findAny() + .orElse(new NoopTranslator()); + } + log.debug("ControlServiceCoordinator.initTranslator(): Translator implementation selected: {}", translator); + + log.info("ControlServiceCoordinator.initTranslator(): Effective translator: {}", translator.getClass().getName()); + } + + // ------------------------------------------------------------------------------------------------------------ + + public String getAppModelPath() { + return currentAppModelId; + } + + public String getCpModelPath() { + return currentCpModelId; + } + + public TranslationContext getTranslationContextOfAppModel(String appModelId) { + return appModelToTcCache.get(_normalizeModelId(appModelId)); + } + + public void setCurrentEmsState(@NonNull EMS_STATE newState, String message) { + this.currentEmsState = newState; + this.currentEmsStateMessage = message; + this.currentEmsStateChangeTimestamp = System.currentTimeMillis(); + + eventBus.send(COORDINATOR_STATUS_TOPIC, Map.of( + "state", newState, + "message", Objects.requireNonNullElse(message, ""), + "timestamp", currentEmsStateChangeTimestamp + ), this); + } + + // ------------------------------------------------------------------------------------------------------------ + + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { + log.debug("ControlServiceCoordinator.applicationReady(): invoked"); + log.info("ControlServiceCoordinator.applicationReady(): IP setting: {}", properties.getIpSetting()); + preloadModels(); + } + + @Async + public void preloadModels() { + String preloadAppModel = properties.getPreload().getCamelModel(); + String preloadCpModel = properties.getPreload().getCpModel(); + if (StringUtils.isNotBlank(preloadAppModel)) { + log.info("==================================================================================================="); + log.info("ControlServiceCoordinator.preloadModels(): Preloading models: app-model={}, cp-model={}", + preloadAppModel, preloadCpModel); + processAppModel(preloadAppModel, preloadCpModel, ControlServiceRequestInfo.EMPTY); + } else { + log.info("ControlServiceCoordinator.preloadModels(): No model to preload"); + } + } + + // ------------------------------------------------------------------------------------------------------------ + + @Async + public void processAppModel(String appModelId, String cpModelId, ControlServiceRequestInfo requestInfo) { + _lockAndProcessModel(appModelId, cpModelId, requestInfo, "processAppModel()", () -> { + // Call '_processNewModels()' to do actual processing + _processAppModels(appModelId, cpModelId, requestInfo); + this.currentAppModelId = _normalizeModelId(appModelId); + this.currentCpModelId = _normalizeModelId(cpModelId); + }); + } + + @Async + public void processCpModel(String cpModelId, ControlServiceRequestInfo requestInfo) { + _lockAndProcessModel(null, cpModelId, requestInfo, "processCpModel()", () -> { + // Call '_processCpModel()' to do actual processing + _processCpModel(cpModelId, requestInfo); + this.currentCpModelId = _normalizeModelId(cpModelId); + }); + } + + @Async + public void setConstants(@NonNull Map constants, ControlServiceRequestInfo requestInfo) { + _lockAndProcessModel(null, null, requestInfo, "processCpModel()", () -> { + // Call '_processCpModel()' to do actual processing + _setConstants(constants, requestInfo); + }); + } + + protected void _lockAndProcessModel(String appModelId, String cpModelId, ControlServiceRequestInfo requestInfo, String caller, Runnable callback) { + // Acquire lock of this coordinator + if (!inUse.compareAndSet(false, true)) { + String mesg = "ControlServiceCoordinator."+caller+": ERROR: Coordinator is in use. Exits immediately"; + log.warn(mesg); + if (!properties.isSkipEsbNotification()) { + sendErrorNotification(appModelId, requestInfo, mesg, mesg); + } else { + log.warn("ControlServiceCoordinator."+caller+": Skipping ESB notification due to configuration"); + } + return; + } + + try { + callback.run(); + } catch (Exception ex) { + setCurrentEmsState(EMS_STATE.ERROR, ex.getMessage()); + + String mesg = "ControlServiceCoordinator."+caller+": EXCEPTION: " + ex; + log.error(mesg, ex); + if (!properties.isSkipEsbNotification()) { + sendErrorNotification(appModelId, requestInfo, mesg, mesg); + } else { + log.warn("ControlServiceCoordinator"+caller+": Skipping ESB notification due to configuration"); + } + } finally { + // Release lock of this coordinator + inUse.compareAndSet(true, false); + } + } + + // ------------------------------------------------------------------------------------------------------------ + + protected void _processAppModels(String appModelId, String cpModelId, ControlServiceRequestInfo requestInfo) { + log.info("ControlServiceCoordinator._processAppModel(): BEGIN: app-model-id={}, cp-model-id={}, request-info={}", appModelId, cpModelId, requestInfo); + + // Translate model into Translation Context (with EPL rules etc.) + TranslationContext _TC; + if (!properties.isSkipTranslation()) { + _TC = translateAppModelAndStore(appModelId); + } else { + log.warn("ControlServiceCoordinator._processAppModel(): Skipping translation due to configuration"); + _TC = loadStoredTranslationContext(appModelId); + } + + // Run TranslationContext plugins + if (translationContextPlugins!=null && translationContextPlugins.size()>0) { + log.info("ControlServiceCoordinator._processAppModel(): Running {} TranslationContext plugins", translationContextPlugins.size()); + translationContextPlugins.stream().filter(Objects::nonNull).forEach(plugin -> { + log.debug("ControlServiceCoordinator._processAppModel(): Calling TranslationContext plugin: {}", plugin.getClass().getName()); + plugin.processTranslationContext(_TC); + log.debug("ControlServiceCoordinator._processAppModel(): RESULTS after running TranslationContext plugin: {}\n{}", plugin.getClass().getName(), _TC); + }); + } else { + log.info("ControlServiceCoordinator._processAppModel(): No TranslationContext plugins found"); + } + + // Print resulting Translation Context + try { + translationContextPrinter.printResults(_TC, null); + } catch (Exception e) { + log.error("ControlServiceCoordinator._processAppModel(): EXCEPTION while printing Translation results: ", e); + } + + // Retrieve Metric Variable Values (MVV) from CP model - i.e. constants + Map constants = new HashMap<>(); + if (!properties.isSkipMvvRetrieve()) { + if (StringUtils.isNotBlank(cpModelId)) { + constants = retrieveConstantsFromCpModel(cpModelId, _TC, EMS_STATE.INITIALIZING); + } else { + log.warn("ControlServiceCoordinator._processAppModel(): No CP model has been provided"); + } + } else { + log.warn("ControlServiceCoordinator._processAppModel(): Skipping MVV retrieval due to configuration"); + } + + // (Re-)Configure Broker and CEP + String upperwareGrouping = properties.getUpperwareGrouping(); + if (!properties.isSkipBrokerCep()) { + configureBrokerCep(appModelId, _TC, constants, upperwareGrouping); + } else { + log.warn("ControlServiceCoordinator._processAppModel(): Skipping Broker-CEP setup due to configuration"); + } + + // Process placeholders in sink type configurations + if (brokerCep!=null && brokerCep.getBrokerCepProperties()!=null) { + String brokerUrlForClients = brokerCep.getBrokerCepProperties().getBrokerUrlForClients(); + processPlaceholdersInMonitors(_TC, brokerUrlForClients); + } + + // (Re-)Configure Baguette server + if (!properties.isSkipBaguette()) { + configureBaguetteServer(appModelId, _TC, constants, upperwareGrouping); + } else { + log.warn("ControlServiceCoordinator._processAppModel(): Skipping Baguette Server setup due to configuration"); + } + + // Start/Stop Netdata collector + if (!properties.isSkipCollectors() && !properties.isSkipBaguette() && nodeRegistry!=null) { + startNetdataCollector(appModelId); + } else { + log.warn("ControlServiceCoordinator._processAppModel(): Skipping Collectors setup due to configuration"); + } + + // (Re-)Configure MetaSolver + if (!properties.isSkipMetasolver()) { + configureMetaSolver(_TC, requestInfo.getJwtToken()); + } else { + log.warn("ControlServiceCoordinator._processAppModel(): Skipping MetaSolver setup due to configuration"); + } + + // Cache _TC in order to reply to Adapter queries about component-to-sensor mappings and sensor-configuration + log.info("ControlServiceCoordinator._processAppModel(): Cache translation results: app-model-id={}", appModelId); + appModelToTcCache.put(_normalizeModelId(appModelId), _TC); + + // Notify ESB, if 'notificationUri' is provided + if (!properties.isSkipEsbNotification()) { + notifyESB(appModelId, requestInfo, EMS_STATE.INITIALIZING); + } else { + log.warn("ControlServiceCoordinator._processAppModel(): Skipping ESB notification due to configuration"); + } + + this.currentTC = _TC; + log.info("ControlServiceCoordinator._processAppModel(): END: app-model-id={}", appModelId); + + setCurrentEmsState(EMS_STATE.READY, null); + } + + protected void _processCpModel(String cpModelId, ControlServiceRequestInfo requestInfo) { + log.info("ControlServiceCoordinator._processCpModel(): BEGIN: cp-model-id={}, request-info={}", cpModelId, requestInfo); + log.info("ControlServiceCoordinator._processCpModel(): Current app-model-id={}", currentAppModelId); + TranslationContext _TC = this.currentTC; + + // Retrieve Metric Variable Values (MVV) from CP model + Map constants = new HashMap<>(); + if (!properties.isSkipMvvRetrieve()) { + constants = retrieveConstantsFromCpModel(cpModelId, _TC, EMS_STATE.RECONFIGURING); + } else { + log.warn("ControlServiceCoordinator._processCpModel(): Skipping MVV retrieval due to configuration"); + } + + // Set MVV constants in Broker-CEP and Baguette Server, and then notify ESB + _setConstants(constants, requestInfo); + + log.info("ControlServiceCoordinator._processCpModel(): END: cp-model-id={}", cpModelId); + + setCurrentEmsState(EMS_STATE.READY, null); + } + + protected void _setConstants(@NonNull Map constants, ControlServiceRequestInfo requestInfo) { + log.info("ControlServiceCoordinator.setConstants(): BEGIN: constants={}, request-info={}", constants, requestInfo); + log.info("ControlServiceCoordinator.setConstants(): constants={}", constants); + + // Retrieve Metric Variable Values (MVV) from CP model + if (properties.isSkipMvvRetrieve()) { + log.warn("ControlServiceCoordinator.setConstants(): isSkipMvvRetrieve is true, but constants processing will continue"); + } + + // (Re-)Configure Broker and CEP + if (!properties.isSkipBrokerCep()) { + reconfigureBrokerCep(constants); + } else { + log.warn("ControlServiceCoordinator.setConstants(): Skipping Broker-CEP setup due to configuration"); + } + + // (Re-)Configure Baguette server + if (!properties.isSkipBaguette()) { + reconfigureBaguetteServer(constants); + } else { + log.warn("ControlServiceCoordinator.setConstants(): Skipping Baguette Server setup due to configuration"); + } + + // Notify ESB, if 'notificationUri' is provided + if (!properties.isSkipEsbNotification()) { + notifyESB(null, requestInfo, EMS_STATE.RECONFIGURING); + } else { + log.warn("ControlServiceCoordinator.setConstants(): Skipping ESB notification due to configuration"); + } + + log.info("ControlServiceCoordinator.setConstants(): END: constants={}", constants); + + setCurrentEmsState(EMS_STATE.READY, null); + } + + private TranslationContext translateAppModelAndStore(String appModelId) { + final TranslationContext _TC; + setCurrentEmsState(EMS_STATE.INITIALIZING, "Retrieving and translating model"); + + // Translate application model into a TranslationContext object + log.info("ControlServiceCoordinator.translateAppModelAndStore(): Model translation: model-id={}", appModelId); + _TC = translator.translate(appModelId); + log.debug("ControlServiceCoordinator.translateAppModelAndStore(): Model translation: RESULTS: {}", _TC); + + // Run post-translation plugins + if (postTranslationPlugins!=null && postTranslationPlugins.size()>0) { + log.info("ControlServiceCoordinator.translateAppModelAndStore(): Running {} post-translation plugins", postTranslationPlugins.size()); + postTranslationPlugins.stream().filter(Objects::nonNull).forEach(plugin -> { + log.debug("ControlServiceCoordinator.translateAppModelAndStore(): Calling post-translation plugin: {}", plugin.getClass().getName()); + plugin.processTranslationResults(_TC, applicationContext.getBean(TopicBeacon.class)); + log.debug("ControlServiceCoordinator.translateAppModelAndStore(): RESULTS after running post-translation plugin: {}\n{}", plugin.getClass().getName(), _TC); + }); + } else { + log.info("ControlServiceCoordinator.translateAppModelAndStore(): No post-translation plugins found"); + } + + // Serialize and store 'TranslationContext' in a file + String fileName = properties.getTcSaveFile(); + if (StringUtils.isNotBlank(fileName)) { + try { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Storing translation context to file"); + + // Get TC file name + fileName = getTcFileName(appModelId, fileName); + if (Paths.get(fileName).toFile().exists()) { + log.warn("ControlServiceCoordinator.translateAppModelAndStore(): The specified Translation Context file already exists. Its contents will be overwritten: tc-file-pattern={}, tc-file={}", properties.getTcLoadFile(), fileName); + } + + // Store _TC in a file + log.debug("ControlServiceCoordinator.translateAppModelAndStore(): Start serializing _TC data in file: {}", fileName); + com.google.gson.Gson gson = new GsonBuilder().setPrettyPrinting().create(); + java.io.Writer writer = new java.io.FileWriter(fileName); + gson.toJson(_TC, writer); + writer.close(); + +// ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); +// mapper.writeValue(Paths.get(fileName+".yml").toFile(), _TC); + + log.debug("ControlServiceCoordinator.translateAppModelAndStore(): Serialized _TC data in file: {}", fileName); + log.info("ControlServiceCoordinator.translateAppModelAndStore(): Saved translation data in file: {}", fileName); + + } catch (IOException ex) { + log.error("ControlServiceCoordinator.translateAppModelAndStore(): FAILED to serialize _TC to file: {} : Exception: ", fileName, ex); + } + } + return _TC; + } + + private TranslationContext loadStoredTranslationContext(String appModelId) { + TranslationContext _TC; + + // deserialize 'TranslationContext' from file + String fileName = properties.getTcLoadFile(); + if (StringUtils.isNotBlank(fileName)) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Loading translation context from file"); + + try { + fileName = getTcFileName(appModelId, fileName); + if (! Paths.get(fileName).toFile().exists()) { + log.error("ControlServiceCoordinator.loadStoredTranslationContext(): The specified Translation Context file does not exist: tc-file-pattern={}, tc-file={}", properties.getTcLoadFile(), fileName); + throw new IllegalArgumentException("The specified Translation Context file does not exist. Check property: control.tc-load-file=" + properties.getTcLoadFile() + ", file-name=" + fileName); + } + log.info("ControlServiceCoordinator.loadStoredTranslationContext(): Loading translator data from file: {}", fileName); + log.debug("ControlServiceCoordinator.loadStoredTranslationContext(): Start deserializing _TC data from file: {}", fileName); + java.io.Reader reader = new java.io.FileReader(fileName); + com.google.gson.Gson gson = new GsonBuilder() + .registerTypeAdapter(Monitor.class, new TranslationContextMonitorGsonDeserializer()) + .create(); + _TC = gson.fromJson(reader, TranslationContext.class); + reader.close(); + log.debug("ControlServiceCoordinator.loadStoredTranslationContext(): Deserialized _TC data from file: {}", fileName); + } catch (IOException ex) { + log.error("ControlServiceCoordinator.loadStoredTranslationContext(): FAILED to deserialize _TC from file: {} : Exception: ", fileName, ex); + throw new IllegalArgumentException("Failed to load translation data from file: " + fileName, ex); + } + } else { + log.error("ControlServiceCoordinator.loadStoredTranslationContext(): No translation context file has been set"); + throw new IllegalArgumentException("No translation context file has been set"); + } + return _TC; + } + + private String getTcFileName(@NonNull String appModelId, @NonNull String fileName) { + appModelId = StringUtils.removeStart(appModelId, "/"); + return String.format(fileName, appModelId.replaceAll("[^\\p{L}\\d]", "_")); + } + + private Map retrieveConstantsFromCpModel(String cpModelId, TranslationContext _TC, EMS_STATE emsState) { + Map constants = Collections.emptyMap(); + if (StringUtils.isNotBlank(cpModelId)) { + setCurrentEmsState(emsState, "Retrieving MVVs from CP model"); + + try { + log.debug("ControlServiceCoordinator.retrieveConstantsFromCpModel(): Retrieving MVVs from CP model: cp-model-id={}", cpModelId); + + // Retrieve constant names from '_TC.MVV_CP' and values from a given CP model + log.debug("ControlServiceCoordinator.retrieveConstantsFromCpModel(): Looking for MVV_CP's: {}", _TC.getCompositeMetricVariables()); + constants = mvvService.getMatchingMetricVariableValues(cpModelId, _TC); + log.debug("ControlServiceCoordinator.retrieveConstantsFromCpModel(): MVVs retrieved from CP model: cp-model-id={}, MVVs={}", cpModelId, constants); + + } catch (Exception ex) { + log.error("ControlServiceCoordinator.retrieveConstantsFromCpModel(): EXCEPTION while retrieving MVVs from CP model: cp-model-id={}", cpModelId, ex); + } + } else { + log.error("ControlServiceCoordinator.retrieveConstantsFromCpModel(): No CP model have been provided"); + } + return constants; + } + + private void configureBrokerCep(String appModelId, TranslationContext _TC, Map constants, String upperwareGrouping) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "initializing Broker-CEP"); + + try { + // Initializing Broker-CEP module if necessary + if (brokerCep == null) { + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Initializing..."); + brokerCep = applicationContext.getBean(BrokerCepService.class); + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Initializing...ok"); + } + + // Get event types for GLOBAL grouping (i.e. that of Upperware) + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Upperware grouping: {}", upperwareGrouping); + Set eventTypeNames = _TC.getG2T().get(upperwareGrouping); + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Configuration of Event Types: {}", eventTypeNames); + if (eventTypeNames == null || eventTypeNames.size() == 0) + throw new RuntimeException("Broker-CEP: No event types for GLOBAL grouping"); + + // Clear any previous event types, statements or function definitions and register the new ones + brokerCep.clearState(); + brokerCep.addEventTypes(eventTypeNames, EventMap.getPropertyNames(), EventMap.getPropertyClasses()); + + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Constants: {}", constants); + brokerCep.setConstants(constants); + + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Function definitions: {}", _TC.getFunctionDefinitions()); + brokerCep.addFunctionDefinitions(_TC.getFunctionDefinitions()); + + Map> ruleStatements = _TC.getG2R().get(upperwareGrouping); + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Configuration of EPL statements: {}", ruleStatements); + if (ruleStatements != null) { + int cnt = 0; + for (Map.Entry> topicRules : ruleStatements.entrySet()) { + String topicName = topicRules.getKey(); + for (String rule : topicRules.getValue()) { + brokerCep.getCepService().addStatementSubscriber( + new BrokerCepStatementSubscriber("Subscriber_" + cnt++, topicName, rule, brokerCep, passwordUtil) + ); + } + } + log.debug("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: Added {} EPL statements", cnt); + } else { + log.warn("ControlServiceCoordinator.configureBrokerCep(): Broker-CEP: No EPL statements found for GLOBAL grouping"); + } + } catch (Exception ex) { + log.error("ControlServiceCoordinator.configureBrokerCep(): EXCEPTION while initializing Broker-CEP of Upperware: app-model-id={}", appModelId, ex); + } + } + + private void reconfigureBrokerCep(Map constants) { + try { + setCurrentEmsState(EMS_STATE.RECONFIGURING, "Reconfiguring Broker-CEP"); + + // Initializing Broker-CEP module if necessary + if (brokerCep == null) { + log.debug("ControlServiceCoordinator.reconfigureBrokerCep(): Broker-CEP: Initializing..."); + brokerCep = applicationContext.getBean(BrokerCepService.class); + log.debug("ControlServiceCoordinator.reconfigureBrokerCep(): Broker-CEP: Initializing...ok"); + } + + log.debug("ControlServiceCoordinator.reconfigureBrokerCep(): Passing constants to Broker-CEP: {}", constants); + brokerCep.setConstants(constants); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.reconfigureBrokerCep(): EXCEPTION while initializing Broker-CEP with constants: constants={}", constants, ex); + } + } + + private static void processPlaceholdersInMonitors(TranslationContext _TC, String brokerUrlForClients) { + for (Monitor mon : _TC.getMON()) { + if (mon.getSinks()!=null) { + for (Sink s : mon.getSinks()) { + s.getConfiguration().entrySet().forEach(entry -> { + if (entry.getValue() != null) + entry.setValue(entry.getValue().replace("%{BROKER_URL}%", brokerUrlForClients)); + }); + } + } + } + } + + private void configureBaguetteServer(String appModelId, TranslationContext _TC, Map constants, String upperwareGrouping) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Initializing Baguette Server"); + + log.debug("ControlServiceCoordinator.configureBaguetteServer(): Re-configuring Baguette Server: app-model-id={}", appModelId); + try { + baguetteServer.setTopologyConfiguration(_TC, constants, upperwareGrouping, brokerCep); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.configureBaguetteServer(): EXCEPTION while starting Baguette server: app-model-id={}", appModelId, ex); + } + } + + private void reconfigureBaguetteServer(Map constants) { + setCurrentEmsState(EMS_STATE.RECONFIGURING, "Reconfiguring Baguette Server"); + + log.debug("ControlServiceCoordinator.reconfigureBaguetteServer(): Re-configuring Baguette Server with constants: {}", constants); + try { + baguetteServer.sendConstants(constants); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.reconfigureBaguetteServer(): EXCEPTION while configuring Baguette server: constants={}", constants, ex); + } + } + + private void startNetdataCollector(String appModelId) { + // Stop any running Netdata collector instance + if (netdataCollector!=null) { + log.info("ControlServiceCoordinator.startNetdataCollector(): Stopping NetdataCollector: app-model-id={}", appModelId); + try { + netdataCollector.stop(); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.startNetdataCollector(): EXCEPTION while stopping NetdataCollector: app-model-id={}", appModelId, ex); + } + } + + // Starting new Netdata collector instance, if needed + ServerCoordinator serverCoordinator = nodeRegistry.getCoordinator(); + if (! serverCoordinator.supportsAggregators()) { + if (netdataCollector==null) { + netdataCollector = applicationContext.getBean(ServerNetdataCollector.class); + } + log.info("ControlServiceCoordinator.startNetdataCollector(): Starting NetdataCollector: app-model-id={}", appModelId); + try { + netdataCollector.start(); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.startNetdataCollector(): EXCEPTION while starting NetdataCollector: app-model-id={}", appModelId, ex); + } + } else { + log.info("ControlServiceCoordinator.startNetdataCollector(): NetdataCollector is not needed (will not start it): app-model-id={}", appModelId); + } + } + + private void configureMetaSolver(TranslationContext _TC, String jwtToken) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Sending configuration to MetaSolver"); + + // Check that MetaSolver configuration URL has been set + if (StringUtils.isEmpty(properties.getMetasolverConfigurationUrl())) { + log.warn("ControlServiceCoordinator.configureMetaSolver(): MetaSolver endpoint is empty. Skipping Metasolver configuration"); + return; + } + + // Get scaling event and SLO topics from _TC + Set scalingTopics = new HashSet<>(); + scalingTopics.addAll(_TC.getE2A().keySet()); + scalingTopics.addAll(_TC.getSLO()); + log.debug("ControlServiceCoordinator.configureMetaSolver(): MetaSolver configuration: scaling-topics: {}", scalingTopics); + + // Get top-level metric topics from _TC + Set metricTopics = _TC.getDAG().getTopLevelNodes().stream() + .map(DAGNode::getElementName) + .filter(elementName -> !scalingTopics.contains(elementName)) + .collect(Collectors.toSet()); + log.debug("ControlServiceCoordinator.configureMetaSolver(): MetaSolver configuration: metric-topics: {}", metricTopics); + + // Prepare subscription configurations + String upperwareBrokerUrl = brokerCep != null ? brokerCep.getBrokerCepProperties().getBrokerUrlForClients() : null; + boolean usesAuthentication = brokerCep.getBrokerCepProperties().isAuthenticationEnabled(); + String username = usesAuthentication ? brokerCep.getBrokerUsername() : null; + String password = usesAuthentication ? brokerCep.getBrokerPassword() : null; + String certificate = brokerCep.getBrokerCertificate(); + log.debug("ControlServiceCoordinator.configureMetaSolver(): Local Broker: uses-authentication={}, username={}, password={}, has-certificate={}", + usesAuthentication, username, passwordUtil.encodePassword(password), StringUtils.isNotBlank(certificate)); + log.trace("ControlServiceCoordinator.configureMetaSolver(): Local Broker: broker-certificate={}", certificate); + + if (StringUtils.isBlank(upperwareBrokerUrl)) { + log.warn("ControlServiceCoordinator.configureMetaSolver(): No Broker URL has been specified or Broker-CEP module is deactivated"); + } + List> subscriptionConfigs = new ArrayList<>(); + for (String t : scalingTopics) + subscriptionConfigs.add(_prepareSubscriptionConfig(upperwareBrokerUrl, username, password, certificate, t, "", "SCALE")); + for (String t : metricTopics) + subscriptionConfigs.add(_prepareSubscriptionConfig(upperwareBrokerUrl, username, password, certificate, t, "", "MVV")); + log.debug("ControlServiceCoordinator.configureMetaSolver(): MetaSolver subscriptions configuration: {}", subscriptionConfigs); + + // Retrieve MVV to Current-Config MVV map + Map mvvMap = _TC.getCompositeMetricVariables(); + log.debug("ControlServiceCoordinator.configureMetaSolver(): MetaSolver MVV configuration: {}", mvvMap); + + // Prepare MetaSolver configuration + Map msConfig = new HashMap<>(); + msConfig.put("subscriptions", subscriptionConfigs); + msConfig.put("mvv", mvvMap); + + // POST configuration to MetaSolver + String metaSolverEndpoint = properties.getMetasolverConfigurationUrl(); + com.google.gson.Gson gson = new com.google.gson.Gson(); + String json = gson.toJson(msConfig); + log.debug("ControlServiceCoordinator.configureMetaSolver(): MetaSolver configuration in JSON: {}", json); + + try { + log.info("ControlServiceCoordinator.configureMetaSolver(): Calling MetaSolver: endpoint={}", metaSolverEndpoint); + ResponseEntity response = webClient.post() + .uri(metaSolverEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, jwtToken) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .bodyValue(json) + .retrieve() + .toEntity(String.class) + .block(); + String metaSolverResponse = (response!=null && response.getStatusCode().is2xxSuccessful()) ? response.getBody() : null; + log.info("ControlServiceCoordinator.configureMetaSolver(): MetaSolver response: endpoint={}, status={}, message={}", + metaSolverEndpoint, response!=null ? response.getStatusCode() : null, metaSolverResponse); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.configureMetaSolver(): Failed to call MetaSolver: endpoint={}, EXCEPTION: ", metaSolverEndpoint, ex); + } + } + + private void notifyESB(String appModelId, ControlServiceRequestInfo requestInfo, @NonNull EMS_STATE emsState) { + if (StringUtils.isNotBlank(requestInfo.getNotificationUri())) { + setCurrentEmsState(emsState, "Notifying ESB"); + + String notificationUri = requestInfo.getNotificationUri().trim(); + log.debug("ControlServiceCoordinator.notifyESB(): Notifying ESB: {}", notificationUri); + sendSuccessNotification(appModelId, requestInfo); + log.debug("ControlServiceCoordinator.notifyESB(): ESB notified: {}", notificationUri); + } else { + log.warn("ControlServiceCoordinator.notifyESB(): Notification URI is blank"); + } + } + + // ------------------------------------------------------------------------------------------------------------ + + protected String _normalizeModelId(String modelId) { + if (StringUtils.isBlank(modelId)) return modelId; + modelId = modelId.trim(); + if (!modelId.startsWith("/")) modelId = "/"+modelId; + return modelId; + } + + protected Map _prepareSubscriptionConfig(String url, String username, String password, String certificate, String topic, String clientId, String type) { + Map map = new HashMap<>(); + map.put("url", url); + map.put("username", username); + map.put("password", password); + map.put("certificate", certificate); + map.put("topic", topic); + map.put("client-id", clientId); + map.put("type", type); + return map; + } + + // ------------------------------------------------------------------------------------------------------------ + // ESB notification methods + // ------------------------------------------------------------------------------------------------------------ + + private void sendSuccessNotification(String applicationId, ControlServiceRequestInfo requestInfo) { + // Prepare success result notification + NotificationResultImpl result = new NotificationResultImpl(); + result.setStatus(NotificationResult.StatusType.SUCCESS); + + // Prepare and send CamelModelNotification + try { + sendAppModelNotification(applicationId, result, requestInfo); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void sendErrorNotification(String applicationId, ControlServiceRequestInfo requestInfo, + String errorCode, String errorDescription) + { + // Prepare error result notification + NotificationResultImpl result = new NotificationResultImpl(); + result.setStatus(NotificationResult.StatusType.ERROR); + result.setErrorCode(errorCode); + result.setErrorDescription(errorDescription); + + // Prepare and send CamelModelNotification + try { + sendAppModelNotification(applicationId, result, requestInfo); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void sendAppModelNotification(String applicationId, NotificationResult result, ControlServiceRequestInfo requestInfo) { + // Create a new watermark + Watermark watermark = new WatermarkImpl(); + watermark.setUser("EMS"); + watermark.setSystem("EMS"); + watermark.setDate(new java.util.Date()); + String uuid = Objects.requireNonNullElse( requestInfo.getRequestUuid(), UUID.randomUUID().toString().toLowerCase() ); + watermark.setUuid(uuid); + + // Create a new CamelModelNotification + CamelModelNotificationRequest request = new CamelModelNotificationRequestImpl(); + request.setApplicationId(applicationId); + request.setResult(result); + request.setWatermark(watermark); + + // Send CamelModelNotification to ESB (Control Process) + sendAppModelNotification(request, requestInfo); + } + + private void sendAppModelNotification(CamelModelNotificationRequest notification, ControlServiceRequestInfo requestInfo) { + String notificationUri = requestInfo.getNotificationUri(); + String requestUuid = requestInfo.getRequestUuid(); + String jwtToken = requestInfo.getJwtToken(); + + // Check if 'notificationUri' is blank + if (StringUtils.isBlank(notificationUri)) { + log.warn("ControlServiceCoordinator.sendAppModelNotification(): notificationUri not provided or is empty. No notification will be sent to ESB."); + return; + } + notificationUri = notificationUri.trim(); + + // Get ESB url from control-service configuration + String esbUrl = properties.getEsbUrl(); + if (StringUtils.isBlank(esbUrl)) { + log.warn("ControlServiceCoordinator.sendAppModelNotification(): esb-url property is empty. No notification will be sent to ESB."); + return; + } + esbUrl = esbUrl.trim(); + + // Fixing ESB URL parts + if (esbUrl.endsWith("/")) { + esbUrl = esbUrl.substring(0, esbUrl.length() - 1); + } + if (notificationUri.startsWith("/")) { + notificationUri = notificationUri.substring(1); + } + + // Call ESB endpoint + String url = esbUrl + "/" + notificationUri; + log.info("ControlServiceCoordinator.sendAppModelNotification(): Invoking ESB endpoint: {}", url); + log.trace("ControlServiceCoordinator.sendAppModelNotification(): JWT token: {}", jwtToken); + + ResponseEntity response; + response = webClient.post(). + uri(url) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, jwtToken) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header("X-Request-UUID", requestUuid) + .bodyValue(notification) + .retrieve() + .toEntity(String.class) + .block(); + + if (response!=null) { + String responseStatus = response.getStatusCode().toString(); + if (response.getStatusCode().is2xxSuccessful()) + log.info("ControlServiceCoordinator.sendAppModelNotification(): ESB endpoint invoked: {}, status={}, message={}", url, responseStatus, response.getBody()); + else + log.info("ControlServiceCoordinator.sendAppModelNotification(): ESB endpoint invoked: {}, status={}, message={}", url, responseStatus, response.getBody()); + } else { + log.warn("ControlServiceCoordinator.sendAppModelNotification(): ESB endpoint invoked: {}, response is NULL", url); + } + } + + public HttpEntity createHttpEntity(Class notificationType, Object notification, String jwtToken) { + HttpHeaders headers = createHttpHeaders(jwtToken); + return new HttpEntity(notificationType.cast(notification), headers); + } + + private HttpHeaders createHttpHeaders(String jwtToken) { + HttpHeaders headers = new HttpHeaders(); + if (StringUtils.isNotBlank(jwtToken)) { + headers.set(HttpHeaders.AUTHORIZATION, jwtToken); + } + headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + return headers; + } + + // ------------------------------------------------------------------------------------------------------------ +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ControlServiceRequestInfo.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ControlServiceRequestInfo.java new file mode 100644 index 0000000..08b5e4d --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ControlServiceRequestInfo.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import lombok.Builder; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +@Builder +public class ControlServiceRequestInfo { + public final static ControlServiceRequestInfo EMPTY = create(null, null, null); + + private final String notificationUri; + private final String requestUuid; + @ToString.Exclude + private final String jwtToken; + + public static ControlServiceRequestInfo create(String notificationUri, String requestUuid, String jwtToken) { + return ControlServiceRequestInfo.builder() + .notificationUri(notificationUri) + .requestUuid(requestUuid) + .jwtToken(jwtToken) + .build(); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/CredentialsController.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/CredentialsController.java new file mode 100644 index 0000000..fe09e82 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/CredentialsController.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.baguette.server.properties.BaguetteServerProperties; +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import gr.iccs.imu.ems.control.webconf.WebSecurityConfig; +import gr.iccs.imu.ems.util.CredentialsMap; +import gr.iccs.imu.ems.util.PasswordUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class CredentialsController { + + private final static String ROLES_ALLOWED_JWT_TOKEN_OR_API_KEY = + "hasAnyRole('"+WebSecurityConfig.ROLE_JWT_TOKEN+"','"+WebSecurityConfig.ROLE_API_KEY+"')"; + + private final ControlServiceProperties properties; + private final ControlServiceCoordinator coordinator; + private final CredentialsCoordinator credentialsCoordinator; + private final WebSecurityConfig webSecurityConfig; + private final PasswordUtil passwordUtil; + + // ------------------------------------------------------------------------------------------------------------ + // Credentials methods + // ------------------------------------------------------------------------------------------------------------ + +// @PreAuthorize(ROLES_ALLOWED_JWT_TOKEN_OR_API_KEY) + @RequestMapping(value = "/broker/credentials", method = {GET,POST}) + public HttpEntity getBrokerCredentials(@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.info("CredentialsController.getBrokerCredentials(): BEGIN"); + log.trace("CredentialsController.getBrokerCredentials(): JWT token: {}", jwtToken); + + // Retrieve sensor information + String brokerClientsUrl = coordinator.getBrokerCep().getBrokerCepProperties().getBrokerUrlForClients(); + String brokerUsername = coordinator.getBrokerCep().getBrokerUsername(); + String brokerPassword = coordinator.getBrokerCep().getBrokerPassword(); + String brokerCertificatePem = coordinator.getBrokerCep().getBrokerCertificate(); + + // Prepare response + Map response = new HashMap<>(); + response.put("url", brokerClientsUrl); + response.put("username", brokerUsername); + response.put("password", brokerPassword); + response.put("certificate", brokerCertificatePem); + HttpEntity entity = coordinator.createHttpEntity(Map.class, response, jwtToken); + log.info("CredentialsController.getBrokerCredentials(): Response: {}", response); + + //return response; + return entity; + } + +// @PreAuthorize(ROLES_ALLOWED_JWT_TOKEN_OR_API_KEY) + @RequestMapping(value = "/baguette/ref/{ref}", method = {GET,POST}, + produces = MediaType.APPLICATION_JSON_VALUE) + public HttpEntity getNodeCredentials(@PathVariable("ref") Optional optRef, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.info("CredentialsController.getNodeCredentials(): BEGIN: ref={}", optRef); + log.trace("CredentialsController.getNodeCredentials(): JWT token: {}", jwtToken); + + if (StringUtils.isBlank(optRef.orElse(null))) + throw new IllegalArgumentException("The 'ref' parameter is mandatory"); + + // Check if it is EMS server ref + if (credentialsCoordinator.getReference().equals(optRef.get())) { + if (coordinator.getBaguetteServer()==null || !coordinator.getBaguetteServer().isServerRunning()) { + log.warn("CredentialsController.getNodeCredentials(): Baguette Server is not started"); + return null; + } + + BaguetteServerProperties config = coordinator.getBaguetteServer().getConfiguration(); + String address = config.getServerAddress(); + int port = config.getServerPort(); + String username = null; + String password = null; + CredentialsMap credentials = config.getCredentials(); + if (!credentials.isEmpty()) { + username = credentials.keySet().stream().findFirst().orElse(null); + password = credentials.get(username); + } + String key = coordinator.getBaguetteServer().getServerPubkey(); + + log.debug("CredentialsController.getNodeCredentials(): Retrieved EMS server connection info by reference: ref={}", optRef.get()); + + // Prepare response + Map response = new HashMap<>(); + response.put("hostname", address); + response.put("port", ""+port); + response.put("username", username); + response.put("password", password); + response.put("private-key", key); + HttpEntity entity = coordinator.createHttpEntity(Map.class, response, jwtToken); + log.debug("CredentialsController.getNodeCredentials(): Response: ** Not shown because it contains credentials **"); + + return entity; + } + + // Retrieve node credentials + NodeRegistryEntry entry = coordinator.getBaguetteServer().getNodeRegistry().getNodeByReference(optRef.get()); + if (entry==null) { + throw new IllegalArgumentException("Not found Node with reference: "+optRef.get()); + } + log.debug("CredentialsController.getNodeCredentials(): Retrieved node by reference: ref={}", optRef.get()); + + // Prepare response + Map response = new HashMap<>(); + response.put("hostname", entry.getIpAddress()); + response.put("port", entry.getPreregistration().getOrDefault("ssh.port", "22")); + response.put("username", entry.getPreregistration().get("ssh.username")); + response.put("password", entry.getPreregistration().get("ssh.password")); + response.put("private-key", entry.getPreregistration().get("ssh.key")); + HttpEntity entity = coordinator.createHttpEntity(Map.class, response, jwtToken); + log.debug("CredentialsController.getNodeCredentials(): Response: ** Not shown because it contains credentials **"); + + return entity; + } + + + // ------------------------------------------------------------------------------------------------------------ + // EMS One-Time-Password (OTP) endpoints + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/ems/otp/new", method = {GET, POST}) + public String newOtp() { + log.info("CredentialsController.newOtp(): BEGIN"); + String newOtp = webSecurityConfig.otpCreate(); + log.debug("CredentialsController.newOtp(): New OTP: {}", passwordUtil.encodePassword(newOtp)); + return newOtp; + } + + @RequestMapping(value = "/ems/otp/remove/{otp}", method = {GET, POST}) + public String removeOtp(@PathVariable String otp) { + log.info("CredentialsController.removeOtp(): BEGIN"); + if ("*".equals(otp)) + webSecurityConfig.otpClearCache(); + else + webSecurityConfig.otpRemove(otp); + log.debug("CredentialsController.removeOtp(): Removed OTP: {}", passwordUtil.encodePassword(otp)); + return "OK"; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/CredentialsCoordinator.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/CredentialsCoordinator.java new file mode 100644 index 0000000..8e9c597 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/CredentialsCoordinator.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CredentialsCoordinator { + @Getter + private final String reference = UUID.randomUUID().toString(); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ManagementController.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ManagementController.java new file mode 100644 index 0000000..6bb3977 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ManagementController.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class ManagementController { + + private final ControlServiceProperties properties; + private final ManagementCoordinator coordinator; + + // ------------------------------------------------------------------------------------------------------------ + // Client and Cluster info and control methods + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/client/list", method = GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public List listClients() { + List clients = coordinator.clientList(); + log.info("ManagementController.listClients(): {}", clients); + return clients; + } + + @RequestMapping(value = "/client/list/map", method = GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public Map> listClientMaps() { + Map> clients = coordinator.clientMap(); + log.info("ManagementController.listClientMaps(): {}", clients); + return clients; + } + + @RequestMapping(value = "/client/command/{clientId}/{command:.+}", method = GET) + public String clientCommand(@PathVariable String clientId, @PathVariable String command) { + log.info("ManagementController.clientCommand(): PARAMS: client={}, command={}", clientId, command); + return coordinator.clientCommandSend(clientId, command); + } + + @RequestMapping(value = "/cluster/command/{clusterId}/{command:.+}", method = GET) + public String clusterCommand(@PathVariable String clusterId, @PathVariable String command) { + log.info("ManagementController.clusterCommand(): PARAMS: cluster={}, command={}", clusterId, command); + return coordinator.clusterCommandSend(clusterId, command); + } + + // ------------------------------------------------------------------------------------------------------------ + // Event Generation and Debugging methods + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/event/generate-start/{clientId}/{topicName}/{interval}/{lowerValue}/{upperValue}", method = GET) + public String startEventGeneration(@PathVariable String clientId, @PathVariable String topicName, @PathVariable long interval, @PathVariable double lowerValue, @PathVariable double upperValue) { + log.info("ManagementController.startEventGeneration(): PARAMS: client={}, topic={}, interval={}, value-range=[{},{}]", clientId, topicName, interval, lowerValue, upperValue); + return coordinator.eventGenerationStart(clientId, topicName, interval, lowerValue, upperValue); + } + + @RequestMapping(value = "/event/generate-stop/{clientId}/{topicName}", method = GET) + public String stopEventGeneration(@PathVariable String clientId, @PathVariable String topicName) { + log.info("ManagementController.stopEventGeneration(): PARAMS: client={}, topic={}", clientId, topicName); + return coordinator.eventGenerationStop(clientId, topicName); + } + + @RequestMapping(value = "/event/send/{clientId}/{topicName}/{value}", method = GET) + public String sendEvent(@PathVariable String clientId, @PathVariable String topicName, @PathVariable double value) { + log.info("ManagementController.sendEvent(): PARAMS: client={}, topic={}, value={}", clientId, topicName, value); + return coordinator.eventLocalSend(clientId, topicName, value); + } + + // ------------------------------------------------------------------------------------------------------------ + // EMS status and information query methods + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/ems/shutdown", method = {GET, POST}) + public String emsShutdown() { + log.info("ManagementController.emsShutdown(): Not implemented"); + coordinator.emsShutdownServices(); + return "OK"; + } + + @RequestMapping(value = { "/ems/exit", "/ems/exit/{exitCode}" }, method = {GET, POST}) + public String emsExit(@PathVariable Optional exitCode) { + if (properties.isExitAllowed()) { + int _exitCode = exitCode.orElse(properties.getExitCode()); + log.info("ManagementController.emsExit(): exitCode={}", _exitCode); + coordinator.emsShutdownServices(); + coordinator.emsExit(_exitCode); + return "OK"; + } else { + log.info("ManagementController.emsExit(): Exiting EMS is not allowed"); + return "NOT ALLOWED"; + } + } + + @RequestMapping(value = "/ems/status", method = {GET, POST}, produces = MediaType.APPLICATION_JSON_VALUE) + public Map emsStatus() { + log.info("ManagementController.emsStatus(): Not implemented"); + return Collections.emptyMap(); + } + + @RequestMapping(value = "/ems/topology", method = {GET, POST}, produces = MediaType.APPLICATION_JSON_VALUE) + public Map emsTopology() { + log.info("ManagementController.emsTopology(): Not implemented"); + return Collections.emptyMap(); + } + + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/health", method = GET) + public String health() { + return "OK"; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ManagementCoordinator.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ManagementCoordinator.java new file mode 100644 index 0000000..6ee9d52 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/ManagementCoordinator.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.brokerclient.event.EventGenerator; +import gr.iccs.imu.ems.control.ControlServiceApplication; +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ManagementCoordinator { + + private final ApplicationContext applicationContext; + private final ControlServiceProperties properties; + private final ControlServiceCoordinator coordinator; + private final BrokerCepService brokerCepService; + private final BaguetteServer baguetteServer; + + private final Map eventGenerators = new HashMap<>(); + + // ------------------------------------------------------------------------------------------------------------ + // Life-Cycle control methods + // ------------------------------------------------------------------------------------------------------------ + + void emsShutdownServices() { + /*log.info("ManagementCoordinator.emsShutdownServices(): Shutting down EMS..."); + log.info("ManagementCoordinator.emsShutdownServices(): Shutting down EMS... done");*/ + log.warn("ManagementCoordinator.emsShutdownServices(): Not implemented"); + } + + void emsExit() { + emsExit(properties.getExitCode()); + } + + void emsExit(int exitCode) { + if (properties.isExitAllowed()) { + // Signal SpringBootApp to exit + log.info("ManagementCoordinator.emsExit(): Signaling exit..."); + ControlServiceApplication.exitApp(exitCode, properties.getExitGracePeriod()); + log.info("ManagementCoordinator.emsExit(): Signaling exit... done"); + } else { + log.warn("ManagementCoordinator.emsExit(): Exit is not allowed"); + } + } + + // ------------------------------------------------------------------------------------------------------------ + // Event Generation and Debugging methods + // ------------------------------------------------------------------------------------------------------------ + + private final static String EVENT_LOG_OK = "OK"; + private final static String EVENT_LOG_ERROR = "ERROR"; + private final static String BAGUETTE_DISABLED = "BAGUETTE SERVER IS DISABLED"; + private final static String BAGUETTE_NOT_RUNNING = "BAGUETTE SERVER IS NOT RUNNING"; + + private String eventLogEnd(String method, String result) { + log.debug("ManagementCoordinator.{}(): END: result={}", method, result); + return result; + } + + private String eventSendCommandToClient(String method, String clientId, String command) { + // Check status + if (properties.isSkipBaguette()) return eventLogEnd(method, BAGUETTE_DISABLED); + if (!baguetteServer.isServerRunning()) return eventLogEnd(method, BAGUETTE_NOT_RUNNING); + + // Send command + if (clientId.equals("0")) { + if (command.startsWith("SEND-")) { + try { + String[] part = command.split(" "); + String topicName = part[1].trim(); + String value = part[2].trim(); + EventMap event = new EventMap(Double.parseDouble(value), 3, System.currentTimeMillis()); + coordinator.getBrokerCep().publishEvent(null, topicName, event); + } catch (Exception ex) { + log.warn("ManagementCoordinator.{}(): EXCEPTION: command: {}, exception: ", method, command, ex); + // Log error + return eventLogEnd(method, EVENT_LOG_ERROR+": "+method+": "+ex.getMessage()); + } + } else if (command.startsWith("GENERATE-EVENTS-START")) { + String[] args = command.split("[ \t\r\n]+"); + String destination = args[1].trim(); + long interval = Long.parseLong(args[2].trim()); + double lower = Double.parseDouble(args[3].trim()); + double upper = Double.parseDouble(args[4].trim()); + if (eventGenerators.get(destination) == null) { + EventGenerator generator = applicationContext.getBean(EventGenerator.class); + //generator.setBrokerUrl(null); + generator.setBrokerUsername(brokerCepService.getBrokerUsername()); + generator.setBrokerPassword(brokerCepService.getBrokerPassword()); + generator.setDestinationName(destination); + generator.setLevel(1); + generator.setInterval(interval); + generator.setLowerValue(lower); + generator.setUpperValue(upper); + eventGenerators.put(destination, generator); + generator.start(); + } + + } else if (command.startsWith("GENERATE-EVENTS-STOP")) { + String[] args = command.split("[ \t\r\n]+"); + String destination = args[1].trim(); + EventGenerator generator = eventGenerators.remove(destination); + if (generator != null) { + generator.stop(); + } + } else { + log.warn("ManagementCoordinator.{}(): ERROR: Unsupported command for client-id=0 : {}", method, command); + // Log error + return eventLogEnd(method, EVENT_LOG_ERROR+": "+method+": "+command); + } + } else if ("*".equals(clientId)) + baguetteServer.sendToActiveClients(command); + else + baguetteServer.sendToClient("#"+clientId, command); + + // Log success + return eventLogEnd(method, EVENT_LOG_OK); + } + + + // Public API for event debugging + public String eventGenerationStart(String clientId, String topicName, long interval, double lowerValue, double upperValue) { + log.debug("ManagementCoordinator.eventGenerationStart(): client={}, topic={}, interval={}, value-range=[{},{}]", clientId, topicName, interval, lowerValue, upperValue); + String command = String.format(java.util.Locale.ROOT, "GENERATE-EVENTS-START %s %d %f %f", topicName, interval, lowerValue, upperValue); + return eventSendCommandToClient("eventGenerationStart", clientId, command); + } + + public String eventGenerationStop(String clientId, String topicName) { + log.debug("ManagementCoordinator.eventGenerationStop(): client={}, topic={}", clientId, topicName); + String command = String.format(java.util.Locale.ROOT, "GENERATE-EVENTS-STOP %s", topicName); + return eventSendCommandToClient("eventGenerationStop", clientId, command); + } + + public String eventLocalSend(String clientId, String topicName, double value) { + log.debug("ManagementCoordinator.eventLocalSend(): BEGIN: client={}, topic={}, value={}", clientId, topicName, value); + String command = String.format(java.util.Locale.ROOT, "SEND-LOCAL-EVENT %s %f", topicName, value); + return eventSendCommandToClient("eventLocalSend", clientId, command); + } + + public String eventRemoteSend(String clientId, String brokerUrl, String topicName, double value) { + log.debug("ManagementCoordinator.eventRemoteSend(): BEGIN: client={}, broker-url={}, topic={}, value={}", clientId, brokerUrl, topicName, value); + String command = String.format(java.util.Locale.ROOT, "SEND-EVENT %s %s %f", brokerUrl, topicName, value); + return eventSendCommandToClient("eventRemoteSend", clientId, command); + } + + // ------------------------------------------------------------------------------------------------------------ + + public List clientList() { + log.debug("ManagementCoordinator.clientList(): BEGIN:"); + return baguetteServer.isServerRunning() ? baguetteServer.getActiveClients() : Collections.emptyList(); + } + + public Map> clientMap() { + log.debug("ManagementCoordinator.clientMap(): BEGIN:"); + return baguetteServer.isServerRunning() ? baguetteServer.getActiveClientsMap() : Collections.emptyMap(); + } + + public List passiveClientList() { + log.debug("ManagementCoordinator.passiveClientList(): BEGIN:"); + return baguetteServer.isServerRunning() ? baguetteServer.getPassiveNodes() : Collections.emptyList(); + } + + public Map> passiveClientMap() { + log.debug("ManagementCoordinator.passiveClientMap(): BEGIN:"); + return baguetteServer.isServerRunning() ? baguetteServer.getPassiveNodesMap() : Collections.emptyMap(); + } + + public List allClientList() { + log.debug("ManagementCoordinator.allClientList(): BEGIN:"); + return baguetteServer.isServerRunning() ? baguetteServer.getAllNodes() : Collections.emptyList(); + } + + public Map> allClientMap() { + log.debug("ManagementCoordinator.allClientMap(): BEGIN:"); + return baguetteServer.isServerRunning() ? baguetteServer.getAllNodesMap() : Collections.emptyMap(); + } + + public String clientCommandSend(String clientId, String command) { + log.debug("ManagementCoordinator.clientCommandSend(): BEGIN: client={}, command={}", clientId, command); + return eventSendCommandToClient("clientCommandSend", clientId, command); + } + + public String clusterCommandSend(String clusterId, String command) { + log.debug("ManagementCoordinator.clusterCommandSend(): BEGIN: cluster={}, command={}", clusterId, command); + return sendCommandToCluster("clusterCommandSend", clusterId, command); + } + + private String sendCommandToCluster(String method, String clusterId, String command) { + // Check status + if (properties.isSkipBaguette()) return eventLogEnd(method, BAGUETTE_DISABLED); + if (!baguetteServer.isServerRunning()) return eventLogEnd(method, BAGUETTE_NOT_RUNNING); + + // Send command + if ("*".equals(clusterId)) + baguetteServer.sendToActiveClusters(command); + else + baguetteServer.sendToCluster(clusterId, command); + + // Log success + return eventLogEnd(method, EVENT_LOG_OK); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/NodeRegistrationController.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/NodeRegistrationController.java new file mode 100644 index 0000000..81abb19 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/NodeRegistrationController.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Map; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class NodeRegistrationController { + private final ControlServiceProperties properties; + private final ControlServiceCoordinator coordinator; + private final NodeRegistrationCoordinator nodeRegistrationCoordinator; + + // ------------------------------------------------------------------------------------------------------------ + // Baguette control methods + // ------------------------------------------------------------------------------------------------------------ + + @RequestMapping(value = "/baguette/stopServer", method = {GET, POST}) + public String baguetteStopServer() { + log.info("NodeRegistrationController.baguetteStopServer(): Request received"); + + // Dispatch Baguette stop operation in a worker thread + nodeRegistrationCoordinator.stopBaguette(); + log.info("NodeRegistrationController.baguetteStopServer(): Baguette stop operation dispatched to a worker thread"); + + return "OK"; + } + + @RequestMapping(value = "/baguette/registerNode", method = POST, + consumes = MediaType.APPLICATION_JSON_VALUE) + public String baguetteRegisterNode(@RequestBody String jsonNode, HttpServletRequest request) throws Exception { + log.info("NodeRegistrationController.baguetteRegisterNode(): Invoked"); + log.debug("NodeRegistrationController.baguetteRegisterNode(): Node json:\n{}", jsonNode); + + // Extract node information from json + Type type = new TypeToken>(){}.getType(); + Map nodeMap = new Gson().fromJson(jsonNode, type); + String nodeId = (String) nodeMap.get("id"); + log.info("NodeRegistrationController.baguetteRegisterNode(): node-id: {}", nodeId); + log.debug("NodeRegistrationController.baguetteRegisterNode(): Node information: map={}", nodeMap); + + String response = nodeRegistrationCoordinator.registerNode(request, nodeMap, + coordinator.getTranslationContextOfAppModel(coordinator.getCurrentAppModelId())); + + log.info("NodeRegistrationController.baguetteRegisterNode(): Node registered: node-id: {}", nodeId); + log.debug("NodeRegistrationController.baguetteRegisterNode(): node: {}, json: {}", nodeId, response); + return response; + } + + @RequestMapping(value = "/baguette/node/list", method = GET) + public Collection baguetteNodeList() throws Exception { + log.info("NodeRegistrationController.baguetteNodeList(): Invoked"); + + Collection addresses = coordinator.getBaguetteServer().getNodeRegistry().getNodeAddresses(); + + log.info("NodeRegistrationController.baguetteNodeList(): {}", addresses); + return addresses; + } + + @RequestMapping(value = "/baguette/node/reinstall/{ipAddress:.+}", method = {GET, POST}, + produces = MediaType.TEXT_PLAIN_VALUE) + public String baguetteNodeReinstall(@PathVariable String ipAddress) throws Exception { + log.info("NodeRegistrationController.baguetteNodeReinstall(): Invoked"); + log.info("NodeRegistrationController.baguetteNodeReinstall(): Node IP address: {}", ipAddress); + + // Get node info using IP address + BaguetteServer baguette = coordinator.getBaguetteServer(); + NodeRegistryEntry nodeInfo = baguette.getNodeRegistry().getNodeByAddress(ipAddress); + log.info("NodeRegistrationController.baguetteNodeReinstall(): Info for node at: ip-address={}, Node Info:\n{}", + ipAddress, nodeInfo); + if (nodeInfo==null) { + log.warn("NodeRegistrationController.baguetteNodeReinstall(): Not found pre-registered node with ip-address: {}", ipAddress); + return "NODE NOT FOUND: "+ipAddress; + } + + // Continue processing according to ExecutionWare type + String response; + log.info("NodeRegistrationController.baguetteNodeReinstall(): ExecutionWare: {}", properties.getExecutionware()); + if (properties.getExecutionware() == ControlServiceProperties.ExecutionWare.CLOUDIATOR) { + response = nodeRegistrationCoordinator.getClientInstallationInstructions(nodeInfo); + } else { + response = nodeRegistrationCoordinator.createClientInstallationTask(nodeInfo, + coordinator.getTranslationContextOfAppModel(coordinator.getCurrentAppModelId())); + } + + log.info("NodeRegistrationController.baguetteNodeReinstall(): node ip-address: {}, response: {}", ipAddress, response); + return response; + } + + @RequestMapping(value = "/baguette/getNodeInfoByAddress/{ipAddress:.+}", method = {GET, POST}, + produces = MediaType.APPLICATION_JSON_VALUE) + public NodeRegistryEntry baguetteGetNodeInfoByAddress(@PathVariable String ipAddress) throws Exception { + log.info("NodeRegistrationController.baguetteGetNodeInfoByAddress(): ip-address={}", ipAddress); + + BaguetteServer baguette = coordinator.getBaguetteServer(); + NodeRegistryEntry nodeInfo = baguette.getNodeRegistry().getNodeByAddress(ipAddress); + + log.info("NodeRegistrationController.baguetteGetNodeInfoByAddress(): Info for node at: ip-address={}, Node Info:\n{}", + ipAddress, nodeInfo); + return nodeInfo; + } + + @RequestMapping(value = "/baguette/getNodeNameByAddress/{ipAddress:.+}", method = {GET, POST}, + produces = MediaType.TEXT_PLAIN_VALUE) + public String baguetteGetNodeNameByAddress(@PathVariable String ipAddress) throws Exception { + log.info("NodeRegistrationController.baguetteGetNodeNameByAddress(): ip-address={}", ipAddress); + + BaguetteServer baguette = coordinator.getBaguetteServer(); + NodeRegistryEntry nodeInfo = baguette.getNodeRegistry().getNodeByAddress(ipAddress); + String nodeName = nodeInfo!=null ? nodeInfo.getPreregistration().get("name") : null; + + log.info("NodeRegistrationController.baguetteGetNodeNameByAddress(): Name of node at: ip-address={}, Node name: {}", + ipAddress, nodeName); + return nodeName; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/NodeRegistrationCoordinator.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/NodeRegistrationCoordinator.java new file mode 100644 index 0000000..78c3daf --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/NodeRegistrationCoordinator.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import gr.iccs.imu.ems.baguette.client.install.ClientInstallationTask; +import gr.iccs.imu.ems.baguette.client.install.ClientInstaller; +import gr.iccs.imu.ems.baguette.client.install.helper.InstallationHelperFactory; +import gr.iccs.imu.ems.baguette.server.BaguetteServer; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import gr.iccs.imu.ems.control.properties.StaticResourceProperties; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.NetUtil; +import gr.iccs.imu.ems.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +@Service +@RequiredArgsConstructor +public class NodeRegistrationCoordinator implements InitializingBean { + private final ControlServiceProperties properties; + @Getter + private final BaguetteServer baguetteServer; + private final StaticResourceProperties staticResourceProperties; + private final AtomicBoolean inUse = new AtomicBoolean(); + + @Override + public void afterPropertiesSet() throws Exception { + } + + // ------------------------------------------------------------------------------------------------------------ + // Baguette control methods + // ------------------------------------------------------------------------------------------------------------ + + @Async + public void stopBaguette() { + // Acquire lock of this coordinator + if (!inUse.compareAndSet(false, true)) { + log.warn("NodeRegistrationCoordinator.stopBaguette(): ERROR: Coordinator is in use. Method exits immediately"); + return; + } + + try { + // Stop Baguette server + log.info("NodeRegistrationCoordinator.stopBaguette(): Stopping Baguette server..."); + baguetteServer.stopServer(); + log.info("NodeRegistrationCoordinator.stopBaguette(): Stopping Baguette server... done"); + } catch (Exception ex) { + log.error("NodeRegistrationCoordinator.stopBaguette(): EXCEPTION while stopping Baguette server: ", ex); + } finally { + // Release lock of this coordinator + inUse.compareAndSet(true, false); + } + } + + + // ------------------------------------------------------------------------------------------------------------ + // Node registration methods + // ------------------------------------------------------------------------------------------------------------ + + public String registerNode(HttpServletRequest request, Map nodeMap, TranslationContext translationContext) throws Exception { + // Pre-process node data passed from SAL (before registering node) + Map nodeMapFlattened = StrUtil.deepFlattenMap(nodeMap); + log.trace("NodeRegistrationCoordinator.registerNode(): Flattened node info map: \n{}", nodeMapFlattened); + + // Get web server base URL + String baseUrl = calculateBaseUrl(request); + log.debug("NodeRegistrationCoordinator.registerNode(): baseUrl={}", baseUrl); + + // Update node registration info with OS name, BASE_URL, IP_SETTING, and CLIENT_ID + updateRegistrationInfo(nodeMapFlattened, baseUrl); + log.trace("NodeRegistrationCoordinator.registerNode(): updated flattened node info map: \n{}", nodeMapFlattened); + + // Register node to Baguette server + NodeRegistryEntry entry; + try { + entry = baguetteServer.registerClient(nodeMapFlattened); + } catch (Exception e) { + log.error("NodeRegistrationCoordinator.registerNode(): EXCEPTION while registering node: map={}\n", nodeMap, e); + return "ERROR "+e.getMessage(); + } + + // Continue processing according to ExecutionWare type + String response; + log.info("NodeRegistrationCoordinator.registerNode(): ExecutionWare: {}", properties.getExecutionware()); + if (properties.getExecutionware()==ControlServiceProperties.ExecutionWare.CLOUDIATOR) { + response = getClientInstallationInstructions(entry); + } else { + response = createClientInstallationTask(entry, translationContext); + } + + return response; + } + + void updateRegistrationInfo(Map nodeMap, String baseUrl) { + // Set OS info + String os = StringUtils.isNotBlank(nodeMap.get("operatingSystem.name")) + ? nodeMap.get("operatingSystem.name") + : nodeMap.get("operatingSystem"); + nodeMap.put("operatingSystem", os); + + // Get IP Setting and Client ID + String ipSetting = properties.getIpSetting().toString(); + String clientId = getBaguetteServer().generateClientIdFromNodeInfo(nodeMap); + + // Add to context + nodeMap.put("BASE_URL", baseUrl); + nodeMap.put("CLIENT_ID", clientId); + nodeMap.put("IP_SETTING", ipSetting); + } + + public String getServerIpAddress() { + return (properties.getIpSetting() == ControlServiceProperties.IpSetting.DEFAULT_IP) + ? NetUtil.getDefaultIpAddress() + : NetUtil.getPublicIpAddress(); + } + + public String calculateBaseUrl(HttpServletRequest request) { + String staticResourceContext = staticResourceProperties.getResourceContext(); + staticResourceContext = StringUtils.substringBeforeLast(staticResourceContext,"/**"); + staticResourceContext = StringUtils.substringBeforeLast(staticResourceContext,"/*"); + if (!staticResourceContext.startsWith("/")) staticResourceContext = "/"+staticResourceContext; + /*String baseUrl = + request.getScheme()+"://"+ coordinator.getServerIpAddress() +":"+request.getServerPort()+staticResourceContext;*/ + String baseUrl = ServletUriComponentsBuilder.fromRequestUri(request) + .host(getServerIpAddress()) + .replacePath(staticResourceContext) + .build().toUriString(); + return baseUrl; + } + + // Retained for backward compatibility with Cloudiator + @SneakyThrows + public String getClientInstallationInstructions(NodeRegistryEntry entry) throws IOException { + // Prepare Baguette Client installation instructions for node + final String CLOUDIATOR_HELPER_CLASS = "gr.iccs.imu.ems.extra.cloudiator.CloudiatorInstallationHelper"; + String json = InstallationHelperFactory.getInstance() + .createInstallationHelperBean(CLOUDIATOR_HELPER_CLASS, entry) + .getInstallationInstructionsForOs(entry) + .orElse(Collections.emptyList()) + .stream().findFirst() + .orElse(null); + if (json==null) { + log.warn("NodeRegistrationCoordinator.getClientInstallationInstructions(): No instruction sets: node-map={}", entry.getPreregistration()); + return null; + } + log.debug("NodeRegistrationCoordinator.getClientInstallationInstructions(): instructionsSet: {}", json); + + log.trace("NodeRegistrationCoordinator.getClientInstallationInstructions(): instructionsSet: node-map={}, json:\n{}", entry.getPreregistration(), json); + return json; + } + + public String createClientInstallationTask(NodeRegistryEntry entry, TranslationContext translationContext + ) throws Exception { + //log.info("ControlServiceController.baguetteRegisterNodeForProactive(): INPUT: node-map: {}", nodeMap); + + ClientInstallationTask installationTask = InstallationHelperFactory.getInstance() + .createInstallationHelper(entry) + .createClientInstallationTask(entry, translationContext); + ClientInstaller.instance().addTask(installationTask); + log.debug("NodeRegistrationCoordinator.createClientInstallationTask(): New installation-task: {}", installationTask); + + return "OK"; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/RestControllerException.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/RestControllerException.java new file mode 100644 index 0000000..9db99ef --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/RestControllerException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import lombok.Getter; +import lombok.Setter; + +public class RestControllerException extends RuntimeException { + @Getter @Setter + private int statusCode; + + public RestControllerException() { super(); } + public RestControllerException(String message) { super(message); } + public RestControllerException(Throwable cause) { super(cause); } + public RestControllerException(String message, Throwable cause) { super(message, cause); } + + public RestControllerException(int code) { super(); statusCode = code; } + public RestControllerException(int code, String message) { super(message); statusCode = code; } + public RestControllerException(int code, Throwable cause) { super(cause); statusCode = code; } + public RestControllerException(int code, String message, Throwable cause) { super(message, cause); statusCode = code; } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/RestControllerExceptionHandler.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/RestControllerExceptionHandler.java new file mode 100644 index 0000000..2fc2320 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/controller/RestControllerExceptionHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.controller; + +import gr.iccs.imu.ems.util.StrUtil; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.time.LocalDateTime; + +@Slf4j +@RestControllerAdvice +public class RestControllerExceptionHandler extends ResponseEntityExceptionHandler implements InitializingBean { + + @Override + public void afterPropertiesSet() { + log.debug("RestControllerExceptionHandler initialized"); + } + + @ExceptionHandler(Throwable.class) + private ResponseEntity handleAnyException(Throwable ex, WebRequest request) { + log.warn("RestControllerExceptionHandler: EXCEPTION: context-path={}, error={}", request.getContextPath(), ex.getMessage()); + log.debug("RestControllerExceptionHandler: EXCEPTION: context-path={}, error={}\n", request.getContextPath(), ex.getMessage(), ex); + + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; //BAD_REQUEST; + if (ex instanceof RestControllerException subEx) { + httpStatus = HttpStatus.resolve(subEx.getStatusCode()); + if (httpStatus!=null) { + if (httpStatus.is5xxServerError()) { + log.error("RestControllerExceptionHandler: EXCEPTION: context-path={}\n", request.getContextPath(), ex); + } + } else + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; //BAD_REQUEST; + } + + // Return error response + ErrorType error = new ErrorType(httpStatus, ex); + return new ResponseEntity<>(error, error.getReason()); + } + + @Data + @Setter(AccessLevel.NONE) + public static class ErrorType { + private final int status; + private final HttpStatus reason; + private final LocalDateTime timestamp = LocalDateTime.now(); + private final String exception; + private final String message; + private final String details; + + public ErrorType(HttpStatus httpStatus, Throwable error) { + status = httpStatus.value(); + reason = httpStatus; + exception = error.getClass().getSimpleName(); + message = error.getMessage(); + details = StrUtil.exceptionToDetailsString(error); + } + } +} \ No newline at end of file diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/BuildInfoProvider.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/BuildInfoProvider.java new file mode 100644 index 0000000..92d5cfe --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/BuildInfoProvider.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.InfoProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.StreamSupport; + +@Slf4j +@Component +public class BuildInfoProvider implements ApplicationContextAware, IEmsInfoProvider { + @Autowired + private ControlServiceProperties properties; + @Autowired + private BuildProperties buildProperties; + + private Map infoMap; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.infoMap = new HashMap<>(); + collectBuildInfo(applicationContext, infoMap); + } + + @Override + public Map getMetricValues() { return infoMap; } + + @SneakyThrows + protected void collectBuildInfo(ApplicationContext applicationContext, Map infoMap) { + // Collect info from 'BuildProperties' + print("\n--------------------------------------------------------------------------------"); + print("===== Build Properties ====="); + final Map map = new LinkedHashMap<>(); + StreamSupport.stream(Spliterators.spliteratorUnknownSize(buildProperties.iterator(), Spliterator.ORDERED), false) + .sorted(Comparator.comparing(InfoProperties.Entry::getKey)) + .forEach(e->{ + print(" - {} = {}", e.getKey(), e.getValue()); + map.put(e.getKey(), e.getValue()); + }); + infoMap.put("buildProperties", map); + print("\n--------------------------------------------------------------------------------"); + + // Collect info from bundled files + infoMap.put("versionInfo", + collectInfoFromFile(applicationContext, "Version Info", "classpath:/version.txt")); + print("\n--------------------------------------------------------------------------------"); + infoMap.put("gitInfo", + collectInfoFromFile(applicationContext, "Git Info", "classpath:/git.properties")); + print("\n--------------------------------------------------------------------------------"); + infoMap.put("buildInfo", + collectInfoFromFile(applicationContext, "Build Info", "classpath:/META-INF/build-info.properties")); + print("\n--------------------------------------------------------------------------------"); + } + + protected Map collectInfoFromFile(ApplicationContext applicationContext, String title, String resourceStr) throws IOException { + Map map = new LinkedHashMap<>(); + Resource[] resources = applicationContext.getResources(resourceStr); + if (resources.length>0) { + Resource r = resources[0]; + String linesStr = StreamUtils.copyToString(r.getInputStream(), StandardCharsets.UTF_8); + String s = StringUtils.repeat("=", title.length()+12); + print("\n{}\n===== {} =====\n{}\n=== File: {}\n=== URL: {}\n\n{}\n", s, title, s, r.getFilename(), r.getURL(), linesStr); + Properties p; + try (StringReader sr = new StringReader(linesStr)) { + p = new Properties(); + p.load(sr); + } + for (final String name: p.stringPropertyNames()) + map.put(name, p.getProperty(name)); + } + return map; + } + + protected void print(String formatter, Object...args) { + if (!properties.isPrintBuildInfo()) return; + log.info(formatter, args); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceBuildInfoEndpoint.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceBuildInfoEndpoint.java new file mode 100644 index 0000000..0d46f0f --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceBuildInfoEndpoint.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +@Endpoint(id = "emsBuildInfo") +public class ControlServiceBuildInfoEndpoint { + @Autowired + private BuildInfoProvider buildInfoProvider; + + @ReadOperation + public Map infoMap() { return buildInfoProvider.getMetricValues(); } + + @ReadOperation + public Map info(@Selector String s) { return buildInfoProvider.getMetricValuesFor(s); } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceHealthIndicator.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceHealthIndicator.java new file mode 100644 index 0000000..6cd923f --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceHealthIndicator.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Slf4j +@Component("ems-control-service") +@ConditionalOnEnabledHealthIndicator("controlService") +public class ControlServiceHealthIndicator implements HealthIndicator, ApplicationContextAware { + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + } + + @Override + public Health health() { + Health.Builder status = Health.up() + .withDetail("message", "EMS Control Service is running"); + return status.build(); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceInfoEndpointExtension.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceInfoEndpointExtension.java new file mode 100644 index 0000000..a6b54d9 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceInfoEndpointExtension.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; +import org.springframework.boot.actuate.info.InfoEndpoint; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +@EndpointWebExtension(endpoint = InfoEndpoint.class) +@ConditionalOnAvailableEndpoint(endpoint = InfoEndpoint.class /*, exposure = org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure.WEB*/) +public class ControlServiceInfoEndpointExtension implements InitializingBean { + + private final ApplicationContext applicationContext; + private final InfoEndpoint delegate; + + @Override + public void afterPropertiesSet() throws Exception { + log.info("Info endpoint is enabled and exposed. Added EMS info extension."); + } + + @ReadOperation + public WebEndpointResponse info() { + Map info = new HashMap<>(this.delegate.info()); + info.put("ems-build-info", applicationContext.getBean(BuildInfoProvider.class).getMetricValues()); + info.put("ems-live-info", applicationContext.getBean(IEmsInfoService.class).getServerMetricValues()); + return new WebEndpointResponse<>(info, 200); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceLiveInfoEndpoint.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceLiveInfoEndpoint.java new file mode 100644 index 0000000..9388ccc --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceLiveInfoEndpoint.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +@Endpoint(id = "emsLiveInfo") +@RequiredArgsConstructor +public class ControlServiceLiveInfoEndpoint { + + private final IEmsInfoService emsInfoService; + + @ReadOperation + public Map infoMap() { + return emsInfoService.getServerMetricValues(); + } + + @ReadOperation + public Map info(@Selector String s) { + Map v = emsInfoService.getServerMetricValuesFor(s); + if (v!=null) + return v; + throw new IllegalArgumentException("Unknown EMS info provider: "+s); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceMBean.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceMBean.java new file mode 100644 index 0000000..6e85704 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceMBean.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jmx.export.annotation.*; +import org.springframework.jmx.export.notification.NotificationPublisher; +import org.springframework.jmx.export.notification.NotificationPublisherAware; +import org.springframework.jmx.support.MetricType; +import org.springframework.stereotype.Component; + +import javax.management.Notification; +import java.util.Date; +import java.util.concurrent.atomic.AtomicLong; + +@Slf4j +@Component("emsControl") +@ManagedResource( + objectName = "gr.iccs.imu.ems:category=EmsInfo,name=emsControl", + log = true, +// logFile = "ems_notif.txt", + description="EMS Control Service Bean") +@ManagedNotifications({ + @ManagedNotification(name = "randNumNotif", notificationTypes = { "java.lang.String", "java.lang.Double" }), + @ManagedNotification(name = "timestampNotif", notificationTypes = { "java.lang.String" }) +}) +public class ControlServiceMBean implements NotificationPublisherAware { + private NotificationPublisher notificationPublisher; + private AtomicLong notificationSequence = new AtomicLong(0); + + @ManagedOperation + public void testOk() { + log.warn("!!!!!!!!!!!!!!!!!!!!!!!! testOk"); + } + + @ManagedOperation + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "message", description = "Message param") + }) + public void test2(String mesg) { + log.warn("!!!!!!!!!!!!!!!!!!!!!!!! test2: {}", mesg); + } + + private String attrib; + + @ManagedAttribute + public String getAttrib() { + log.warn("!!!!!!!!!!!!!!!!!!!!!!!! getAttrib: {}", attrib); + return attrib; + } + @ManagedAttribute + public void setAttrib(String s) { + log.warn("!!!!!!!!!!!!!!!!!!!!!!!! setAttrib: {} -> {}", attrib, s); + attrib = new String(s); + } + + @ManagedMetric(category = "ems-metrics", description = "EMS metrics bla bla", displayName = "Curr Date", + metricType = MetricType.COUNTER, unit = "_date") + public Date getCurrDate() { + Date now = new Date(); + log.warn("!!!!!!!!!!!!!!!!!!!!!!!! getCurrDate: {}", now); + return now; + } + + @Override + public void setNotificationPublisher(NotificationPublisher notificationPublisher) { + this.notificationPublisher = notificationPublisher; + } + + @ManagedOperation + public void trigger() { + if (notificationPublisher != null) { + final Notification notification = new Notification("java.lang.String", + getClass().getName(), + notificationSequence.get(), + "A random number: "+(Math.random()*10000000000L)); + notificationPublisher.sendNotification(notification); + log.warn("!!!!!!!!!!!!!!!!!!!!!!!! trigger/1: {}", notification); + + final Notification notification2 = new Notification("java.lang.Double", + "source2", + notificationSequence.getAndIncrement(), + ""+(Math.random()*10000000000L)); + notificationPublisher.sendNotification(notification2); + log.warn("!!!!!!!!!!!!!!!!!!!!!!!! trigger/2: {}", notification2); + } + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceMetrics.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceMetrics.java new file mode 100644 index 0000000..c78a120 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/ControlServiceMetrics.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ControlServiceMetrics implements ApplicationContextAware { + + private final MeterRegistry meterRegistry; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + Counter howmany = Counter + .builder("ems-howmany") + .description("EMS test counter metric") + .tags("ems", "test") + .register(meterRegistry); + howmany.increment(10); + Gauge freemem = Gauge + .builder("ems-freemem", () -> Runtime.getRuntime().freeMemory()) + .description("EMS test gauge metric") + .tags("ems", "test") + .register(meterRegistry); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/EmsInfoServiceImpl.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/EmsInfoServiceImpl.java new file mode 100644 index 0000000..0c5e378 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/EmsInfoServiceImpl.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import gr.iccs.imu.ems.baguette.server.ClientShellCommand; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.common.misc.SystemResourceMonitor; +import gr.iccs.imu.ems.control.controller.ControlServiceCoordinator; +import gr.iccs.imu.ems.control.controller.CredentialsCoordinator; +import gr.iccs.imu.ems.control.controller.ManagementCoordinator; +import gr.iccs.imu.ems.control.controller.NodeRegistrationCoordinator; +import gr.iccs.imu.ems.control.plugin.EmsInfoPlugin; +import gr.iccs.imu.ems.control.properties.ControlServiceProperties; +import gr.iccs.imu.ems.control.properties.InfoServiceProperties; +import gr.iccs.imu.ems.control.properties.StaticResourceProperties; +import gr.iccs.imu.ems.control.properties.WebSecurityProperties; +import gr.iccs.imu.ems.control.util.EventBusCache; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.FunctionDefinition; +import gr.iccs.imu.ems.util.GROUPING; +import gr.iccs.imu.ems.util.NetUtil; +import gr.iccs.imu.ems.util.StrUtil; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmsInfoServiceImpl implements IEmsInfoService { + + private final AtomicLong currentServerMetricsVersion = new AtomicLong(0); + private final AtomicLong currentClientMetricsVersion = new AtomicLong(0); + private Map currentServerMetrics; + private Map currentClientMetrics; + + private final ApplicationContext applicationContext; + private final ControlServiceProperties controlServiceProperties; + private final InfoServiceProperties infoServiceProperties; + private final ControlServiceCoordinator controlServiceCoordinator; + private final CredentialsCoordinator credentialsCoordinator; + private final ManagementCoordinator managementCoordinator; + private final NodeRegistrationCoordinator nodeRegistrationCoordinator; + private final StaticResourceProperties staticResourceProperties; + private final WebSecurityProperties webSecurityProperties; + + private final BuildInfoProvider buildInfoProvider; + private final SystemInfoProvider systemInfoProvider; + private final BrokerCepService brokerCepService; + private final SystemResourceMonitor systemResourceMonitor; + private final EventBusCache eventBusCache; + + private final List emsInfoPlugins; + + @Override + public void clearServerMetricValues() { + log.debug("clearServerMetricValues(): BEGIN"); + synchronized (currentServerMetricsVersion) { + systemInfoProvider.clearMetricValues(); + brokerCepService.clearBrokerCepStatistics(); + currentServerMetrics = null; + + // Call clear on EmsInfoPlugin's + callClearOnPlugins(); + } + log.debug("clearServerMetricValues(): END"); + } + + @Override + public Map getServerMetricValues() { + log.debug("getServerMetricValues(): BEGIN"); + updateServerMetricValues(false); + log.debug("getServerMetricValues(): END: {}", currentServerMetrics); + return currentServerMetrics; + } + + public Map getServerMetricValuesFor(@NonNull String key) { + log.debug("getServerMetricValuesFor(): BEGIN: key={}", key); + return StrUtil.castToMapStringObject(getServerMetricValues().get(key)); + } + + // ------------------------------------------------------------------------ + + @Override + public void clearClientMetricValues() { + log.debug("clearClientMetricValues(): BEGIN"); + synchronized (currentClientMetricsVersion) { + currentClientMetrics = null; + managementCoordinator.clientCommandSend("*", "CLEAR-STATS"); + } + log.debug("clearClientMetricValues(): END"); + } + + @Override + public Map getClientMetricValues() { + log.debug("getClientMetricValues(): BEGIN"); + updateClientMetricValues(); + log.debug("getClientMetricValues(): END: {}", currentClientMetrics); + return currentClientMetrics; + } + + @Override + public Map getClientMetricValues(@NonNull String clientId) { + log.debug("getClientMetricValues(): BEGIN: clientId={}", clientId); + return StrUtil.castToMapStringObject(getClientMetricValues().get(clientId)); + } + + // ------------------------------------------------------------------------ + + protected void updateServerMetricValues(boolean includeStaticInfo) { + log.debug("updateServerMetricValues(): BEGIN: includeStaticInfo={}", includeStaticInfo); + if (currentServerMetrics!=null) { + long timestamp = (long) currentServerMetrics.get(".timestamp"); + log.trace("updateServerMetricValues(): stored-timestamp: {}", timestamp); + if (System.currentTimeMillis() - timestamp < infoServiceProperties.getMetricsUpdateInterval()) { + log.debug("updateServerMetricValues(): STOP: Retry in {}ms", + timestamp + infoServiceProperties.getMetricsUpdateInterval() - System.currentTimeMillis()); + return; + } + } + + long timestamp = System.currentTimeMillis(); + log.trace("updateServerMetricValues(): new-timestamp: {}", timestamp); + + Map metrics = new LinkedHashMap<>(); + + metrics.put("ip-address", nodeRegistrationCoordinator.getServerIpAddress()); + metrics.put("public-ip-address", NetUtil.getPublicIpAddress()); + metrics.put("default-ip-address", NetUtil.getDefaultIpAddress()); + metrics.put("reference", credentialsCoordinator.getReference()); + + // Collect JVM and System resource metrics for EMS server + Map systemInfo = new LinkedHashMap<>(); + systemInfo.put("jmx-resource-metrics", systemInfoProvider.getMetricValues()); + systemInfo.put("system-resource-metrics", systemResourceMonitor.getLatestMeasurements()); + metrics.put(SYSTEM_INFO_PROVIDER, systemInfo); + + // Collect EMS build info + if (includeStaticInfo) + metrics.put(BUILD_INFO_PROVIDER, buildInfoProvider.getMetricValues()); + + // Collect Control Service metrics + Map controlServiceInfo = new LinkedHashMap<>(); + controlServiceInfo.put("current-ems-state", controlServiceCoordinator.getCurrentEmsState()); + controlServiceInfo.put("current-ems-state-message", controlServiceCoordinator.getCurrentEmsStateMessage()); + controlServiceInfo.put("current-ems-state-change-timestamp", controlServiceCoordinator.getCurrentEmsStateChangeTimestamp()); + controlServiceInfo.put("current-app-model-path", controlServiceCoordinator.getAppModelPath()); + controlServiceInfo.put("current-cp-model-path", controlServiceCoordinator.getCpModelPath()); + if (controlServiceProperties!=null && infoServiceProperties!=null) { + controlServiceInfo.put("prop-ip-setting", controlServiceProperties.getIpSetting()); + controlServiceInfo.put("prop-executionware", controlServiceProperties.getExecutionware().toString()); + controlServiceInfo.put("prop-esb-url", controlServiceProperties.getEsbUrl()); + controlServiceInfo.put("prop-metasolver-config-url", controlServiceProperties.getMetasolverConfigurationUrl()); + controlServiceInfo.put("prop-metrics-update-interval", infoServiceProperties.getMetricsUpdateInterval()); + controlServiceInfo.put("prop-metrics-client-update-interval", infoServiceProperties.getMetricsClientUpdateInterval()); + controlServiceInfo.put("prop-metrics-stream-event-name", infoServiceProperties.getMetricsStreamEventName()); + controlServiceInfo.put("prop-metrics-stream-update-interval", infoServiceProperties.getMetricsStreamUpdateInterval()); + controlServiceInfo.put("prop-preload-app-model", controlServiceProperties.getPreload().getCamelModel()); + controlServiceInfo.put("prop-preload-cp-model", controlServiceProperties.getPreload().getCpModel()); + controlServiceInfo.put("prop-upperware-grouping", controlServiceProperties.getUpperwareGrouping()); + controlServiceInfo.put("prop-tc-load-file", controlServiceProperties.getTcLoadFile()); + controlServiceInfo.put("prop-tc-save-file", controlServiceProperties.getTcSaveFile()); + + Map debugFlags = new LinkedHashMap<>(); + debugFlags.put("exit-allowed", controlServiceProperties.isExitAllowed()); + debugFlags.put("print-build-info", controlServiceProperties.isPrintBuildInfo()); + debugFlags.put("skip-translation", controlServiceProperties.isSkipTranslation()); + debugFlags.put("skip-broker-cep-init", controlServiceProperties.isSkipBrokerCep()); + debugFlags.put("skip-baguette-server-init", controlServiceProperties.isSkipBaguette()); + debugFlags.put("skip-mvv-retrieve", controlServiceProperties.isSkipMvvRetrieve()); + debugFlags.put("skip-metasolver-configuration", controlServiceProperties.isSkipMetasolver()); + debugFlags.put("skip-esb-notification", controlServiceProperties.isSkipEsbNotification()); + controlServiceInfo.put("prop-debug-flags",debugFlags); + } + if (staticResourceProperties!=null) { + Map staticResourceCfg = new LinkedHashMap<>(); + /*staticResourceCfg.put("favicon-context", staticResourceProperties.getFaviconContext()); + staticResourceCfg.put("favicon-path", staticResourceProperties.getFaviconPath());*/ + staticResourceCfg.put("resource-context", staticResourceProperties.getResourceContext()); + staticResourceCfg.put("resource-path", staticResourceProperties.getResourcePath()); + staticResourceCfg.put("resource-redirect", staticResourceProperties.getRedirect()); + staticResourceCfg.put("resource-redirects", staticResourceProperties.getRedirects()); + staticResourceCfg.put("logs-context", staticResourceProperties.getLogsContext()); + staticResourceCfg.put("logs-path", staticResourceProperties.getLogsPath()); + controlServiceInfo.put("prop-static-resource", staticResourceCfg); + } + // Adding Authorization properties has been moved to an EmsInfoPlugin in ems-4-morphemic project + if (webSecurityProperties!=null) { + Map authMap = new LinkedHashMap<>(); + authMap.put("jwt-authentication-enabled", webSecurityProperties.getJwtAuthentication().isEnabled()); + authMap.put("api-key-authentication-enabled", webSecurityProperties.getApiKeyAuthentication().isEnabled()); + authMap.put("otp-authentication-enabled", webSecurityProperties.getOtpAuthentication().isEnabled()); + authMap.put("form-authentication-enabled", webSecurityProperties.getFormAuthentication().isEnabled()); + controlServiceInfo.put("prop-authentication-methods", authMap); + } + controlServiceInfo.put("latest-bus-events", eventBusCache.asList()); + metrics.put(CONTROL_INFO_PROVIDER, controlServiceInfo); + + // Collect Broker-CEP metrics + metrics.put(BROKER_CEP_INFO_PROVIDER, brokerCepService.getBrokerCepStatistics()); + + // Collect Baguette-Client metrics and topology + Map baguetteServerInfo = new LinkedHashMap<>(); + baguetteServerInfo.put("active-clients-list", managementCoordinator.clientList()); + baguetteServerInfo.put("active-clients-map", managementCoordinator.clientMap()); + baguetteServerInfo.put("passive-clients-list", managementCoordinator.passiveClientList()); + baguetteServerInfo.put("passive-clients-map", managementCoordinator.passiveClientMap()); + baguetteServerInfo.put("all-clients-list", managementCoordinator.allClientList()); + baguetteServerInfo.put("all-clients-map", managementCoordinator.allClientMap()); + metrics.put(BAGUETTE_SERVER_INFO_PROVIDER, baguetteServerInfo); + + // Destinations per grouping and min/max grouping + Map translatorInfo = new LinkedHashMap<>(); + metrics.put(TRANSLATOR_INFO_PROVIDER, translatorInfo); + String appModelPath = controlServiceCoordinator.getAppModelPath(); + if (StringUtils.isNotBlank(appModelPath)) { + TranslationContext _TC = controlServiceCoordinator.getTranslationContextOfAppModel(appModelPath); + Set groupings = _TC.getG2T().keySet(); + ArrayList orderedGroupings = new ArrayList<>(groupings); + orderedGroupings.sort((o1, o2) -> { + GROUPING g1 = GROUPING.valueOf(o1); + GROUPING g2 = GROUPING.valueOf(o2); + return g1.compareTo(g2); + }); + translatorInfo.put("app-model-path", appModelPath); + translatorInfo.put("groupings", orderedGroupings); + translatorInfo.put("actions-per-event", _TC.getE2A()); + translatorInfo.put("slo", _TC.getSLO()); + translatorInfo.put("monitors", _TC.getMONS()); + translatorInfo.put("rules-per-grouping", _TC.getG2R()); + translatorInfo.put("destinations-per-grouping", _TC.getG2T()); + translatorInfo.put("composite-metric-variables", _TC.getCMVar()); + translatorInfo.put("metric-variable-values", _TC.getMVV()); + translatorInfo.put("metric-variable-values-for-CP", _TC.getCompositeMetricVariables()); + translatorInfo.put("destination-connections", _TC.getTopicConnections()); + translatorInfo.put("function-definitions", _TC.getFUNC().stream() + .map(FunctionDefinition::toString).collect(Collectors.toList())); + translatorInfo.put("export-files", _TC.getExportFiles()); + } + + // Call EmsInfoPlugin's to add information + callUpdateInfoOnPlugins(metrics); + + log.debug("updateServerMetricValues(): Collected server metrics: {}", metrics); + + synchronized (currentServerMetricsVersion) { + log.trace("updateServerMetricValues(): IN-SYNC-BLOCK"); + if (currentServerMetrics==null || (long)currentServerMetrics.get(".timestamp") < timestamp) { + long version = currentServerMetricsVersion.getAndIncrement(); + log.trace("updateServerMetricValues(): NEW-VERSION: {}", version); + metrics.put(".version", version); + metrics.put(".timestamp", timestamp); + this.currentServerMetrics = Collections.unmodifiableMap(metrics); + log.trace("updateServerMetricValues(): NEW currentServerMetrics: {}", currentServerMetrics); + } + log.debug("updateServerMetricValues(): END"); + } + } + + private void callClearOnPlugins() { + log.debug("callClearOnPlugins(): BEGIN: Calling clear on EMS info plugins: {}", emsInfoPlugins); + emsInfoPlugins.forEach(plugin -> { + try { + log.trace("callClearOnPlugins(): - Calling clear on plugin: {}", plugin); + plugin.clearInfo(); + log.trace("callClearOnPlugins(): Plugin clear completed: {}", plugin); + } catch (Exception e) { + log.warn("callClearOnPlugins(): EXCEPTION while calling lear on plugin: {}\n", plugin, e); + } + }); + log.debug("callClearOnPlugins(): END: Calling clear on EMS info plugins"); + } + + private void callUpdateInfoOnPlugins(Map metrics) { + log.debug("callUpdateInfoOnPlugins(): BEGIN: Calling EMS info plugins: {}", emsInfoPlugins); + emsInfoPlugins.forEach(plugin -> { + try { + log.trace("callUpdateInfoOnPlugins(): - Calling plugin: {}", plugin); + plugin.updateInfo(metrics); + log.trace("callUpdateInfoOnPlugins(): Plugin completed: {}, metrics={}", plugin, metrics); + } catch (Exception e) { + log.warn("callUpdateInfoOnPlugins(): EXCEPTION while calling plugin: {}\n", plugin, e); + } + }); + log.debug("callUpdateInfoOnPlugins(): END: Calling EMS info plugins"); + } + + protected void updateClientMetricValues() { + log.debug("updateClientMetricValues(): BEGIN"); + // Not really needed check, since clients PUSH their statistics to server + if (currentClientMetrics!=null) { + long timestamp = (long) currentClientMetrics.get(".timestamp"); + log.trace("updateClientMetricValues(): stored-timestamp: {}", timestamp); + if (System.currentTimeMillis() - timestamp < infoServiceProperties.getMetricsClientUpdateInterval()) { + log.debug("updateClientMetricValues(): STOP: Retry in {}ms", + timestamp + infoServiceProperties.getMetricsClientUpdateInterval() - System.currentTimeMillis()); + return; + } + } + + long timestamp = System.currentTimeMillis(); + log.trace("updateClientMetricValues(): new-timestamp: {}", timestamp); + + Map clientMetrics = new LinkedHashMap<>(); + + // Collecting EMS clients' metrics + List clientIds = managementCoordinator.clientList(); + log.trace("updateClientMetricValues(): active-baguette-clients: {}", clientIds); + for (String clientId : clientIds.stream().map(s->s.split(" ")[0]).toList()) { + /*log.trace("updateClientMetricValues(): Requesting metrics from client: {}", clientId); + Object o = baguetteServer.readFromClient(clientId, "SHOW-STATS", org.slf4j.event.Level.DEBUG); + log.trace("updateClientMetricValues(): Metrics from client: {}, metrics: {}", clientId, o); + if (o instanceof Map) { + clientMetrics.put(clientId, o); + log.trace("updateClientMetricValues(): client-metrics: id={}, Client metrics ADDED in results map", clientId); + }*/ + + log.trace("updateClientMetricValues(): Retrieving cached statistics of client: id={}", clientId); + ClientShellCommand csc = ClientShellCommand.getActiveById(clientId); + log.trace("updateClientMetricValues(): CSC of client: id={}, CSC={}", clientId, csc); + if (csc!=null) { + if (csc.getClientStatistics()!=null) { + clientMetrics.put(clientId, csc.getClientStatistics()); + log.trace("updateClientMetricValues(): client-metrics: id={}, Client metrics ADDED in results map", clientId); + } else { + log.debug("updateClientMetricValues(): No client statistics available: client-id={}", clientId); + } + } else { + log.warn("updateClientMetricValues(): CSC NOT FOUND: client-id={}", clientId); + } + } + log.debug("updateClientMetricValues(): Collected client metrics: {}", clientMetrics); + + synchronized (currentClientMetricsVersion) { + log.trace("updateClientMetricValues(): IN-SYNC-BLOCK"); + if (currentClientMetrics==null || (long)currentClientMetrics.get(".timestamp") < timestamp) { + long version = currentClientMetricsVersion.getAndIncrement(); + log.trace("updateClientMetricValues(): NEW-VERSION: {}", version); + clientMetrics.put(".version", version); + clientMetrics.put(".timestamp", timestamp); + this.currentClientMetrics = Collections.unmodifiableMap(clientMetrics); + log.trace("updateServerMetricValues(): NEW currentClientMetrics: {}", currentClientMetrics); + } + log.debug("updateClientMetricValues(): END"); + } + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/FilesController.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/FilesController.java new file mode 100644 index 0000000..154285f --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/FilesController.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import gr.iccs.imu.ems.control.properties.InfoServiceProperties; +import gr.iccs.imu.ems.util.EmsConstant; +import jakarta.servlet.http.HttpServletRequest; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.HandlerMapping; + +import javax.validation.constraints.Null; +import java.io.*; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@ConditionalOnProperty(value = "enabled", prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "info.files", havingValue = "true", matchIfMissing = true) +public class FilesController { + + private final InfoServiceProperties properties; + private final List roots; + + public FilesController(@NonNull InfoServiceProperties properties) { + this.properties = properties; + List tmp = properties.getFiles().getRoots(); + this.roots = (tmp!=null) ? tmp : Collections.emptyList(); + log.debug("FilesController: File roots: {}", roots); + } + + @GetMapping("/files") + public List listRoots(HttpServletRequest request) { + log.debug("listRoots(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + return toFileList(roots, null); + } + + @GetMapping("/files/tree/roots") + public List> listTreeRoots(HttpServletRequest request) throws IOException { + log.debug("listTreeRoots(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + LinkedList> trees = new LinkedList<>(); + for (Path root : roots) { + trees.add( toFileList(Files.walk(root).collect(Collectors.toList()), root) ); + } + return trees; + } + + @GetMapping("/files/tree/{rootId}") + public List listTreeFiles(HttpServletRequest request, @PathVariable int rootId) throws IOException { + log.debug("listTreeFiles(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + log.debug("listTreeFiles(): --- Root-Id: {}", rootId); + Path root = roots.get(rootId); + return toFileList(Files.walk(root).collect(Collectors.toList()), root); + } + + @GetMapping({"/files/dir/{rootId}", "/files/dir/{rootId}/**"}) + public List listDirFiles(HttpServletRequest request, @PathVariable int rootId, WebRequest webRequest) throws IOException { + log.debug("listDirFiles(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + String mvcPrefix = "/files/dir/" + rootId; + String pathStr = getPathFromRequest(request, webRequest, mvcPrefix); + + Path path = Paths.get(roots.get(rootId).toString(), pathStr); + log.debug("listDirFiles(): --- Effective Path: {}", path); + if (path.toFile().exists()) { + if (path.toFile().isDirectory()) { + return toFileList(Files.list(path).collect(Collectors.toList()), path); + } else { + return null; + } + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "File not found: "+rootId+": "+pathStr); + } + } + + private String getPathFromRequest(HttpServletRequest request, WebRequest webRequest, String mvcPrefix) { + log.debug("getPathFromRequest(): --- mvc-prefix: {}", mvcPrefix); + String mvcPath = (String) webRequest.getAttribute( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + log.debug("getPathFromRequest(): --- mvc-path: {}", mvcPath); + String pathStr = mvcPath!=null ? mvcPath.substring(mvcPrefix.length()) : ""; + log.debug("getPathFromRequest(): --- Prefix: {}, Path: {}", mvcPrefix, pathStr); + return pathStr; + } + + private InputStreamResource toStringInputStream(String s) { + //return new InputStreamResource(new org.apache.tools.ant.filters.StringInputStream(s)); + return new InputStreamResource(new ByteArrayInputStream(s.getBytes())); + } + + @GetMapping("/files/get/{rootId}/**") + public ResponseEntity getFile(HttpServletRequest request, @PathVariable int rootId, WebRequest webRequest) throws IOException { + log.debug("getFile(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + String mvcPrefix = "/files/get/" + rootId + "/"; + String pathStr = getPathFromRequest(request, webRequest, mvcPrefix); + + File file = Paths.get(roots.get(rootId).toString(), pathStr).toFile(); + log.debug("getFile(): --- Effective Path: {}", file); + if (!file.exists()) { + //return ResponseEntity.badRequest().body( toStringInputStream("File not exists") ); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "File not found: "+rootId+": "+pathStr); + } + if (isFileBlocked(file.toPath())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Blocked extension. Cannot download file: "+rootId+": "+pathStr); + } + if (!file.canRead()) { + return ResponseEntity.badRequest().body( toStringInputStream("File cannot be read") ); + } + if (file.isFile()) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="+file.getName()); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + + String mimeType = URLConnection.guessContentTypeFromName(file.getName()); + if (StringUtils.isBlank(mimeType)) + mimeType = Files.probeContentType(file.toPath()); + log.debug("getFile(): --- File content type: {}", mimeType); + MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; + try { + if (StringUtils.isNotBlank(mimeType)) + mediaType = MediaType.parseMediaType(mimeType); + } catch (Exception e) { + log.warn("getFile(): --- Invalid File content type: {}, file: {}\n", mimeType, file.getName(), e); + } + log.debug("getFile(): --- Will use content type: {}", mediaType); + + return ResponseEntity.ok() + .headers(headers) + .contentLength(file.length()) + .contentType(mediaType) + .body(new InputStreamResource(new FileInputStream(file))); + } + return ResponseEntity.badRequest().body( toStringInputStream("Not a regular file") ); + } + + @GetMapping("/files/getpath/**") + public ResponseEntity getFileFromPath(HttpServletRequest request, WebRequest webRequest) throws IOException { + log.debug("getFileFromPath(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + String mvcPrefix = "/files/getpath/"; + String pathStr = getPathFromRequest(request, webRequest, mvcPrefix); + log.debug("getFileFromPath(): --- pathStr: {}", pathStr); + if (!pathStr.startsWith(File.separator)) pathStr = File.separator+pathStr; + + String filePath = null; + for (Path r : roots) { + log.trace("getFileFromPath(): --- Checking pathStr against root: pathStr={}, root={}", pathStr, r); + if (pathStr.startsWith(r.toFile().getAbsolutePath())) { + log.debug("getFileFromPath(): --- pathStr is under root: pathStr={}, root={}", pathStr, r); + filePath = pathStr; + if (!filePath.startsWith(File.separator)) filePath = File.separator+filePath; + break; + } + } + log.debug("getFileFromPath(): --- filePath: {}", filePath); + if (filePath==null) { + log.warn("getFileFromPath(): --- FORBIDDEN: Specified path is not under any allowed root: {}", filePath); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Specified path is not under any allowed root: "+filePath); + } + + File file = Paths.get(pathStr).toFile(); + log.debug("getFileFromPath(): --- Effective Path: {}", file); + if (!file.exists()) { + //return ResponseEntity.badRequest().body( toStringInputStream("File not exists") ); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "File not found: "+pathStr); + } + if (isFileBlocked(file.toPath())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Blocked extension. Cannot download file: "+pathStr); + } + if (!file.canRead()) { + return ResponseEntity.badRequest().body( toStringInputStream("File cannot be read") ); + } + if (file.isFile()) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + + String mimeType = URLConnection.guessContentTypeFromName(file.getName()); + if (StringUtils.isBlank(mimeType)) + mimeType = Files.probeContentType(file.toPath()); + log.debug("getFileFromPath(): --- File content type: {}", mimeType); + MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; + try { + if (StringUtils.isNotBlank(mimeType)) + mediaType = MediaType.parseMediaType(mimeType); + } catch (Exception e) { + log.warn("getFileFromPath(): --- Invalid File content type: {}, file: {}\n", mimeType, file.getName(), e); + } + log.debug("getFileFromPath(): --- Will use content type: {}", mediaType); + if (mediaType==MediaType.APPLICATION_OCTET_STREAM) + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="+file.getName()); + + return ResponseEntity.ok() + .headers(headers) + .contentLength(file.length()) + .contentType(mediaType) + .body(new InputStreamResource(Files.newInputStream(file.toPath()))); + } + return ResponseEntity.badRequest().body( toStringInputStream("Not a regular file") ); + } + + private boolean isFileBlocked(Path path) { + String fileName = path.toFile().getName(); + return properties.getFiles().getExtensionsBlocked().stream() + .anyMatch(ext->StringUtils.endsWithIgnoreCase(fileName, ext)); + } + + private List toFileList(@NonNull List paths, @Null Path root) { + String prefix = (root!=null) ? root.toString() : ""; + boolean listBlocked = properties.getFiles().isListBlocked(); + boolean listHidden = properties.getFiles().isListHidden(); + List list = new LinkedList<>(); + for (Path p : paths) { + boolean blocked = isFileBlocked(p); + if (!listBlocked && blocked) continue; + if (!listHidden && p.toFile().isHidden()) continue; + String pathStr = StringUtils.removeStart(p.toString(), prefix); + File f = p.toFile(); + if (StringUtils.isNotBlank(pathStr)) + list.add(FILE.builder() + .path(pathStr) + .size(f.length()) + .lastModified(f.lastModified()) + .hidden(f.isHidden()) + .dir(f.isDirectory()) + .root(root==null) + .read(f.canRead()).write(f.canWrite()).exec(f.canExecute()) + .noLink(blocked) + .build()); + } + return list; + } + + @Data + @Builder + public static class FILE { + private final String path; + private final long size; + private final long lastModified; + private final boolean hidden; + private final boolean dir; + private final boolean root; + private final boolean read; + private final boolean write; + private final boolean exec; + private final boolean noLink; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/FilesDisabledController.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/FilesDisabledController.java new file mode 100644 index 0000000..0bc1adb --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/FilesDisabledController.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@ConditionalOnMissingBean(FilesController.class) +public class FilesDisabledController { + + public FilesDisabledController() { + log.info("FilesDisabledController: File browsing is disabled"); + } + + @GetMapping({"/files", "/files/**"}) + public ResponseEntity filesDisabled(HttpServletRequest request) { + log.debug("filesDisabled(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + return ResponseEntity.badRequest().body("File browsing is disabled"); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/IEmsInfoProvider.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/IEmsInfoProvider.java new file mode 100644 index 0000000..1888c7d --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/IEmsInfoProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import gr.iccs.imu.ems.util.StrUtil; + +import java.util.Map; + +public interface IEmsInfoProvider { + default void clearMetricValues() { } + + default Map getMetricValues() { return null; } + + default Map getMetricValuesFor(String key) { + return StrUtil.castToMapStringObject(getMetricValues().get(key)); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/IEmsInfoService.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/IEmsInfoService.java new file mode 100644 index 0000000..9bf27ee --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/IEmsInfoService.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import lombok.NonNull; + +import java.util.Map; + +public interface IEmsInfoService { + String SYSTEM_INFO_PROVIDER = "system-info"; + String BUILD_INFO_PROVIDER = "build-info"; + String CONTROL_INFO_PROVIDER = "control"; + String BROKER_CEP_INFO_PROVIDER = "broker-cep"; + String BAGUETTE_SERVER_INFO_PROVIDER = "baguette-server"; + String CLIENT_INSTALLER_INFO_PROVIDER = "baguette-client-installer"; + String TRANSLATOR_INFO_PROVIDER = "translator"; + String MISC_INFO_PROVIDER = "misc-info"; + + void clearServerMetricValues(); + Map getServerMetricValues(); + Map getServerMetricValuesFor(@NonNull String key); + + void clearClientMetricValues(); + Map getClientMetricValues(); + Map getClientMetricValues(@NonNull String clientId); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/InfoServiceController.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/InfoServiceController.java new file mode 100644 index 0000000..7c7a4c4 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/InfoServiceController.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +//XXX: TODO: Temporarily disabled logviewer: import com.logviewer.data2.LogFormat; +//XXX: TODO: Temporarily disabled logviewer: import com.logviewer.logLibs.LogConfigurationLoader; +//XXX: TODO: Temporarily disabled logviewer: import com.logviewer.springboot.LogViewerSpringBootConfig; +import gr.iccs.imu.ems.control.controller.ControlServiceCoordinator; +import gr.iccs.imu.ems.control.controller.ManagementCoordinator; +import gr.iccs.imu.ems.control.plugin.WebAdminPlugin; +import gr.iccs.imu.ems.control.properties.InfoServiceProperties; +import gr.iccs.imu.ems.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.QueryParam; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +//XXX: TODO: Temporarily disabled logviewer: import org.springframework.context.annotation.Bean; +//XXX: TODO: Temporarily disabled logviewer: import org.springframework.context.annotation.Import; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.codec.ServerSentEvent; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +//XXX: TODO: Temporarily disabled logviewer: import java.nio.file.Path; +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequiredArgsConstructor +//XXX: TODO: Temporarily disabled logviewer: @Import(LogViewerSpringBootConfig.class) +public class InfoServiceController implements InitializingBean { + + private final InfoServiceProperties properties; + private final ControlServiceCoordinator coordinator; + private final ManagementCoordinator managementCoordinator; + private final IEmsInfoService emsInfoService; + private final List webAdminPlugins; + private List restCallCommands; + private Map>> restCallForms; + + @Override + public void afterPropertiesSet() throws Exception { + initAdditionalRestCommands(); + } + + /*XXX: TODO: Temporarily disabled logviewer + @Bean + public LogConfigurationLoader getLogConfigurationLoader() { + // Initialize Log-Viewer log paths + List logPaths = properties.getLogViewerFiles(); + if (logPaths==null || logPaths.size()==0) + return null; + return () -> { + LinkedHashMap logConf = new LinkedHashMap<>(); + logPaths.forEach(p -> logConf.put(p, null)); + log.info("LogConfigurationLoader: log-paths: {}", logConf); + return logConf; + }; + }*/ + + @GetMapping("/info/metrics/get") + public Mono> serverMetricsGet(HttpServletRequest request, @AuthenticationPrincipal UserDetails user) { + log.info("serverMetricsGet(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + Map message = createServerMetricsResult(null, -1L, user); + log.debug("serverMetricsGet(): message={}", message); + return Mono.just(message); + } + + @GetMapping("/info/metrics/stream") + public Flux>> serverMetricsStream( + @QueryParam("interval") Optional interval, HttpServletRequest request, @AuthenticationPrincipal UserDetails user) + { + String sid = UUID.randomUUID().toString(); + log.info("serverMetricsStream(): interval={} --- client: {}:{}, Stream-Id: {}", + interval, request.getRemoteAddr(), request.getRemotePort(), sid); + int intervalInSeconds = interval.orElse(-1); + if (intervalInSeconds<1) intervalInSeconds = properties.getMetricsStreamUpdateInterval(); + log.debug("serverMetricsStream(): effective-interval={}", intervalInSeconds); + + return Flux.interval(Duration.ofSeconds(intervalInSeconds)) + .onBackpressureDrop() + .map(sequence -> { + Map message = createServerMetricsResult(sid, sequence, user); + log.debug("serverMetricsStream(): seq={}, id={}, message={}", sequence, sid, message); + return ServerSentEvent.> builder() + .id(String.valueOf(sequence)) + .event(properties.getMetricsStreamEventName()) + .data(message) + .build(); + }); + } + + @GetMapping("/info/metrics/clear") + public String serverMetricsClear(HttpServletRequest request) { + log.info("serverMetricsClear(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + emsInfoService.clearServerMetricValues(); + emsInfoService.clearClientMetricValues(); + return "CLEARED-SERVER-METRICS"; + } + + // ------------------------------------------------------------------------ + + @GetMapping("/info/client-metrics/get/{clientIds}") + public Mono> clientMetricsGet( + @PathVariable("clientIds") List clientIds, HttpServletRequest request) + { + log.info("clientMetricsGet(): baguette-client-ids={} --- client: {}:{}", clientIds, request.getRemoteAddr(), request.getRemotePort()); + Map message = createClientMetricsResult(null, -1L, clientIds); + log.debug("clientMetricsGet(): message={}", message); + return Mono.just(message); + } + + @GetMapping("/info/client-metrics/stream/{clientIds}") + public Flux>> clientMetricsStream( + @PathVariable("clientIds") List clientIds, + @QueryParam("interval") Optional interval, + HttpServletRequest request) + { + String sid = UUID.randomUUID().toString(); + log.info("clientMetricsStream(): interval={}, baguette-client-ids={} --- client: {}:{}, Stream-Id: {}", + interval, clientIds, request.getRemoteAddr(), request.getRemotePort(), sid); + int intervalInSeconds = interval.orElse(-1); + if (intervalInSeconds<1) intervalInSeconds = properties.getMetricsStreamUpdateInterval(); + log.debug("clientMetricsStream(): effective-interval={}", intervalInSeconds); + + return Flux.interval(Duration.ofSeconds(intervalInSeconds)) + .onBackpressureDrop() + .map(sequence -> { + Map message = createClientMetricsResult(sid, sequence, clientIds); + log.debug("clientMetricsStream(): seq={}, id={}, message={}", sequence, sid, message); + return ServerSentEvent.> builder() + .id(String.valueOf(sequence)) + .event(properties.getMetricsStreamEventName()) + .data(message) + .build(); + }); + } + + @GetMapping("/info/client-metrics/clear/{clientIds}") + public String clientMetricsClear(@PathVariable("clientIds") List clientIds, HttpServletRequest request) { + log.info("clientMetricsClear(): baguette-client-ids={} --- client: {}:{}", + clientIds, request.getRemoteAddr(), request.getRemotePort()); + emsInfoService.clearClientMetricValues(); + return "CLEARED-CLIENT-METRICS"; + } + + // ------------------------------------------------------------------------ + + @GetMapping("/info/all-metrics/get/{clientIds}") + public Mono> allMetricsGet( + @PathVariable("clientIds") List clientIds, HttpServletRequest request, @AuthenticationPrincipal UserDetails user) + { + log.info("allMetricsGet(): baguette-client-ids={} --- client: {}:{}", clientIds, request.getRemoteAddr(), request.getRemotePort()); + Map message1 = createServerMetricsResult(null, -1L, user); + Map message2 = createClientMetricsResult(null, -1L, clientIds); + Map message = new LinkedHashMap<>(); + message.put("ems", message1); + message.put("clients", message2); + log.debug("allMetricsGet(): message={}", message); + return Mono.just(message); + } + + @GetMapping("/info/all-metrics/stream/{clientIds}") + public Flux>> allMetricsStream( + @PathVariable("clientIds") List clientIds, + @QueryParam("interval") Optional interval, + HttpServletRequest request, + @AuthenticationPrincipal UserDetails user) + { + String sid = UUID.randomUUID().toString(); + log.info("allMetricsStream(): interval={}, baguette-client-ids={} --- client: {}:{}, Stream-Id: {}", + interval, clientIds, request.getRemoteAddr(), request.getRemotePort(), sid); + int intervalInSeconds = interval.orElse(-1); + if (intervalInSeconds<1) intervalInSeconds = properties.getMetricsStreamUpdateInterval(); + log.debug("allMetricsStream(): effective-interval={}", intervalInSeconds); + + return Flux.interval(Duration.ofSeconds(intervalInSeconds)) + .onBackpressureDrop() + .map(sequence -> { + Map message1 = createServerMetricsResult(sid, sequence, user); + Map message2 = createClientMetricsResult(sid, sequence, clientIds); + Map message = new LinkedHashMap<>(); + message.put("ems", message1); + message.put("clients", message2); + log.debug("allMetricsStream(): seq={}, id={}, message={}", sequence, sid, message); + return ServerSentEvent.> builder() + .id(String.valueOf(sequence)) + .event(properties.getMetricsStreamEventName()) + .data(message) + .build(); + }); + } + + @GetMapping("/info/all-metrics/clear") + public String allMetricsClear(HttpServletRequest request) { + log.info("allMetricsClear(): client: {}:{}", + request.getRemoteAddr(), request.getRemotePort()); + emsInfoService.clearServerMetricValues(); + emsInfoService.clearClientMetricValues(); + return "CLEARED-ALL-METRICS"; + } + + // ------------------------------------------------------------------------ + + public Map createServerMetricsResult(String sid, long sequence, UserDetails userDetails) { + log.trace("createServerMetricsResult: BEGIN: sid={}, seq={}", sid, sequence); + Map metrics = new LinkedHashMap<>(emsInfoService.getServerMetricValues()); + + addMetricsFromEnvVars(metrics); + addAuthenticationInfo(metrics, userDetails); + addRestCallCommands(metrics); + + metrics.put(".stream-id", sid); + metrics.put(".sequence", sequence); + log.trace("createMetricsResult: {}", metrics); + log.trace("createServerMetricsResult: END: sid={}, seq={} ==> {}", sid, sequence, metrics); + return metrics; + } + + public Map createClientMetricsResult(String sid, long sequence, @NonNull List clientIds) { + log.trace("createClientMetricsResult: BEGIN: sid={}, seq={}, client-ids={}", sid, sequence, clientIds); + Map metrics = emsInfoService.getClientMetricValues(); + log.trace("createClientMetricsResult: metrics: {}", metrics); + if (metrics!=null && clientIds.size()>0 && !clientIds.contains("*")) { + clientIds = clientIds.stream() + .filter(StringUtils::isNotBlank) + .map(s->s.startsWith("#") ? s : "#"+s) + .collect(Collectors.toList()); + log.trace("createClientMetricsResult(): CLIENT-FILTER: PREPARE: client-ids: {}", clientIds); + metrics = new LinkedHashMap<>(metrics); + log.trace("createClientMetricsResult(): CLIENT-FILTER: BEFORE: metrics: {}", metrics); + metrics.keySet().retainAll(clientIds); + log.trace("createClientMetricsResult(): CLIENT-FILTER: AFTER: metrics: {}", metrics); + } + + // Add client info in results + Map> clientsInfo = managementCoordinator.clientMap(); + for (Map.Entry entry : metrics.entrySet()) { + Map info = clientsInfo.get(entry.getKey()); + Object o = entry.getValue(); + if (o instanceof Map) { + StrUtil.castToMapStringObject(o) + .put("client-info", info); + } + } + + Map clientMetrics = new LinkedHashMap<>(); + clientMetrics.put("client-metrics", metrics); + clientMetrics.put(".stream-id", sid); + clientMetrics.put(".sequence", sequence); + log.trace("createClientMetricsResult: END: sid={}, seq={} ==> {}", sid, sequence, clientMetrics); + return clientMetrics; + } + + protected void addMetricsFromEnvVars(Map metrics) { + // Process configured env. var. prefixes + for (String prefix : properties.getEnvVarPrefixes()) { + prefix = prefix.trim(); + if (StringUtils.isNotBlank(prefix)) { + // Check for processing switches (at the end of the prefix) + boolean trimPrefix = false; + boolean underscoreToDash = false; + boolean uppercase = false; + boolean lowercase = false; + int len = prefix.length(); + while (len>0) { + char ch = prefix.charAt(len-1); + if (ch=='/') { trimPrefix = true; len--; } + else if (ch=='-') { underscoreToDash = true; len--; } + else if (ch=='^') { uppercase = true; len--; } + else if (ch=='~') { lowercase = true; len--; } + else break; + } + + // Check env. vars against the prefix (and its switches) + if (len>0) { + if (prefix.length()!=len) prefix = prefix.substring(0, len); + + final String _prefix = prefix; + final boolean _trimPrefix = trimPrefix; + final boolean _underscoreToDash = underscoreToDash; + final boolean _uppercase = uppercase; + final boolean _lowercase = lowercase; + System.getenv().forEach((varName,varValue) -> { + if (StringUtils.startsWithIgnoreCase(varName, _prefix)) { + // Process switches + String varNameOriginal = varName; + if (_trimPrefix) varName = varName.substring(_prefix.length()); + if (_underscoreToDash) varName = varName.replace("_", "-"); + if (_uppercase) varName = varName.toUpperCase(); + if (_lowercase) varName = varName.toLowerCase(); + + // Add env. var. in the metrics map + log.debug("addMetricsFromEnvVars: Adding env. var. {} in metrics map as: {} = {}", varNameOriginal, varName, varValue); + metrics.put(varName, varValue); + } + }); + } + } + } + } + + private void addAuthenticationInfo(Map metrics, UserDetails userDetails) { + log.debug("addAuthenticationInfo: user-details: {}", userDetails); + if (userDetails!=null && StringUtils.isNotBlank(userDetails.getUsername())) { + String username = userDetails.getUsername(); + metrics.put(".authentication-username", username); + log.debug("addAuthenticationInfo: Adding username from session: {}", username); + } + } + + private void initAdditionalRestCommands() { + if (webAdminPlugins==null) return; + final List commandGroups = new ArrayList<>(); + final Set formsSet = new HashSet<>(); + webAdminPlugins.stream().filter(Objects::nonNull).forEach(plugin->{ + WebAdminPlugin.RestCallCommandGroup commandGroup = plugin.restCallCommands(); + List cmdList = commandGroup.getCommands(); + if (cmdList!=null && cmdList.size()>0 && StringUtils.isNotBlank(commandGroup.getId())) { + commandGroups.add( Map.of( + "id", commandGroup.getId(), + "text", commandGroup.getText(), + "priority", commandGroup.getPriority(), + "disabled", Boolean.toString(commandGroup.isDisabled()), + "options", cmdList.stream().filter(Objects::nonNull).map(cmd -> Map.of( + "id", cmd.getId(), + "text", cmd.getText(), + "url", cmd.getUrl(), + "method", cmd.getMethod(), + "form", (cmd.getForm() != null && StringUtils.isNotBlank(cmd.getForm().getId())) + ? cmd.getForm().getId() : cmd.getFormId(), + "priority", Integer.toString(cmd.getPriority()), + "disabled", Boolean.toString(cmd.isDisabled()) + )).toList() + + ) ); + formsSet.addAll( cmdList.stream() + .filter(Objects::nonNull) + .map(WebAdminPlugin.RestCallCommand::getForm) + .filter(Objects::nonNull) + .collect(Collectors.toSet()) + ); + } + }); + restCallCommands = commandGroups; + restCallForms = formsSet.stream().collect(Collectors.toMap( + WebAdminPlugin.RestCallForm::getId, + f -> Collections.singletonMap("fields", f.getFields()) + )); + } + + private void addRestCallCommands(Map metrics) { + log.debug("addRestCallCommands: rest-call-commands: {}", restCallCommands); + log.debug("addRestCallCommands: rest-call-forms: {}", restCallForms); + if (restCallCommands!=null && restCallForms!=null) { + metrics.put(".rest-call-commands", restCallCommands); + metrics.put(".rest-call-forms", restCallForms); + log.debug("addRestCallCommands: Added rest-call commands and forms"); + } + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/SystemInfoProvider.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/SystemInfoProvider.java new file mode 100644 index 0000000..6e824c1 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/info/SystemInfoProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.info; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.lang.management.*; +import java.util.LinkedHashMap; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SystemInfoProvider implements IEmsInfoProvider { + + private final File root = new File("/"); + + @Override + public Map getMetricValues() { + Map sysInfo = new LinkedHashMap<>(); + sysInfo.put("jvm-memory-total", Runtime.getRuntime().totalMemory()); + sysInfo.put("jvm-memory-max", Runtime.getRuntime().freeMemory()); + sysInfo.put("jvm-memory-free", Runtime.getRuntime().maxMemory()); + + MemoryMXBean memBean = ManagementFactory.getMemoryMXBean() ; + String heapInfo = memBean.getHeapMemoryUsage().toString(); + String nonHeapInfo = memBean.getNonHeapMemoryUsage().toString(); + sysInfo.put("jvm-memory-heap", heapInfo); + sysInfo.put("jvm-memory-non-heap", nonHeapInfo); + + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + sysInfo.put("jvm-thread-count", threadBean.getThreadCount()); + sysInfo.put("jvm-thread-daemon-count", threadBean.getDaemonThreadCount()); + sysInfo.put("jvm-thread-peak-count", threadBean.getPeakThreadCount()); + sysInfo.put("jvm-thread-total-started-count", threadBean.getTotalStartedThreadCount()); + + RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); + long uptime = runtimeBean.getUptime() / 1000; + String vmInfo = String.format("%s, ver.%s, by %s", runtimeBean.getVmName(), runtimeBean.getVmVersion(), runtimeBean.getVmVendor()); + sysInfo.put("jvm-info", vmInfo); + sysInfo.put("jvm-uptime", uptime); + + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + String osInfo = String.format("OS %s, %s, v.%s, processors: %d, avg. load: %.02f", osBean.getName(), osBean.getArch(), osBean.getVersion(), + osBean.getAvailableProcessors(), osBean.getSystemLoadAverage()); + sysInfo.put("os-info", osInfo); + + sysInfo.put("os-disk-total", root.getTotalSpace()); + sysInfo.put("os-disk-free", root.getFreeSpace()); + sysInfo.put("os-disk-usable", root.getUsableSpace()); + + return sysInfo; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/BeaconPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/BeaconPlugin.java new file mode 100644 index 0000000..0120221 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/BeaconPlugin.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin; + +import gr.iccs.imu.ems.control.util.TopicBeacon; +import gr.iccs.imu.ems.util.Plugin; + +/** + * TopicBeacon plugin + */ +public interface BeaconPlugin extends Plugin { + void transmit(TopicBeacon.BeaconContext context); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/EmsInfoPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/EmsInfoPlugin.java new file mode 100644 index 0000000..100581f --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/EmsInfoPlugin.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin; + +import gr.iccs.imu.ems.util.Plugin; + +import java.util.Map; + +/** + * Executed every time EMS info are collected + */ +public interface EmsInfoPlugin extends Plugin { + void updateInfo(Map metrics); + void clearInfo(); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/PostTranslationPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/PostTranslationPlugin.java new file mode 100644 index 0000000..5a3279a --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/PostTranslationPlugin.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin; + +import gr.iccs.imu.ems.control.util.TopicBeacon; +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.Plugin; + +/** + * Executed right after application model translation, to enrich TranslationContext with additional information, + * but before TranslationContext is stored in a TC JSON file. + * When TranslationContext is loaded from TC file PostTranslationPlugin plugins are NOT executed. If this is desired + * use TranslationContextPlugin plugins instead. + */ +public interface PostTranslationPlugin extends Plugin { + void processTranslationResults(TranslationContext translationContext, TopicBeacon topicBeacon); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/TranslationContextPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/TranslationContextPlugin.java new file mode 100644 index 0000000..de4145c --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/TranslationContextPlugin.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin; + +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.util.Plugin; + +/** + * Executed after application model translation and TranslationContext store in TC JSON file, + * or after TranslationContext loading from a TC JSON file. + * NOTE: + * Changes made by these plugins are NOT stored in TC JSON file + */ +public interface TranslationContextPlugin extends Plugin { + void processTranslationContext(TranslationContext translationContext); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/WebAdminPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/WebAdminPlugin.java new file mode 100644 index 0000000..d139b9a --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/WebAdminPlugin.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin; + +import gr.iccs.imu.ems.util.Plugin; +import lombok.*; + +import java.util.List; + +/** + * WebAdmin plugin + */ +public interface WebAdminPlugin extends Plugin { + RestCallCommandGroup restCallCommands(); + + @Getter + @Builder + @AllArgsConstructor + class RestCallCommandGroup { + @NonNull + private String id; + @NonNull + private String text; + private int priority; + private boolean disabled; + @Singular + private List commands; + } + + @Getter + @Builder + @AllArgsConstructor + class RestCallCommand { + @NonNull + private String id; + @NonNull + private String text; + @NonNull + private String url; + @Builder.Default + private String method = "GET"; + private String formId; + private RestCallForm form; + private int priority; + private boolean disabled; + } + + @Getter + @Builder + @AllArgsConstructor + class RestCallForm { + @NonNull + private String id; + @Singular + private List fields; + } + + @Getter + @Builder + @AllArgsConstructor + class RestCallFormField { + @NonNull + private String name; + @NonNull + private String text; + private String defaultValue; + private boolean function; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopBeaconPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopBeaconPlugin.java new file mode 100644 index 0000000..e6eaf46 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopBeaconPlugin.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin.noop; + +import gr.iccs.imu.ems.control.plugin.BeaconPlugin; +import gr.iccs.imu.ems.control.util.TopicBeacon; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class NoopBeaconPlugin implements BeaconPlugin { + public void transmit(TopicBeacon.BeaconContext context) { + log.trace("NoopBeaconPlugin.transmit(): Invoked"); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopPostTranslationPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopPostTranslationPlugin.java new file mode 100644 index 0000000..06a2d51 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopPostTranslationPlugin.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin.noop; + +import gr.iccs.imu.ems.control.plugin.PostTranslationPlugin; +import gr.iccs.imu.ems.control.util.TopicBeacon; +import gr.iccs.imu.ems.translate.TranslationContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; + +@Slf4j +@Service +public class NoopPostTranslationPlugin implements PostTranslationPlugin { + @PostConstruct + public void created() { + log.debug("NoopPostTranslationPlugin: CREATED"); + } + + @Override + public void processTranslationResults(TranslationContext translationContext, TopicBeacon topicBeacon) { + log.debug("NoopPostTranslationPlugin.processTranslationResults(): INVOKED"); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopTranslationContextPlugin.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopTranslationContextPlugin.java new file mode 100644 index 0000000..d2b9dc8 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/plugin/noop/NoopTranslationContextPlugin.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.plugin.noop; + +import gr.iccs.imu.ems.control.plugin.TranslationContextPlugin; +import gr.iccs.imu.ems.translate.TranslationContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; + +@Slf4j +@Service +public class NoopTranslationContextPlugin implements TranslationContextPlugin { + @PostConstruct + public void created() { + log.debug("NoopTranslationContextPlugin: CREATED"); + } + + @Override + public void processTranslationContext(TranslationContext translationContext) { + log.debug("NoopTranslationContextPlugin.processTranslationContext(): INVOKED"); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/ControlServiceProperties.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/ControlServiceProperties.java new file mode 100644 index 0000000..8e24758 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/ControlServiceProperties.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.properties; + +import gr.iccs.imu.ems.util.KeystoreAndCertificateProperties; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; +//import org.springframework.context.annotation.PropertySource; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Min; + +import static gr.iccs.imu.ems.util.EmsConstant.EMS_PROPERTIES_PREFIX; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EMS_PROPERTIES_PREFIX + "control") +/*@PropertySource(value = { + "file:${EMS_CONFIG_DIR}/ems-server.yml", + "file:${EMS_CONFIG_DIR}/ems-server.properties", + "file:${EMS_CONFIG_DIR}/ems.yml", + "file:${EMS_CONFIG_DIR}/ems.properties" +}, ignoreResourceNotFound = true)*/ +public class ControlServiceProperties { + public enum IpSetting { + DEFAULT_IP, + PUBLIC_IP + } + + public enum ExecutionWare { + CLOUDIATOR, PROACTIVE + } + + private boolean printBuildInfo; + + private IpSetting ipSetting = IpSetting.PUBLIC_IP; + private ExecutionWare executionware = ExecutionWare.PROACTIVE; + + private String upperwareGrouping; + private String metasolverConfigurationUrl; + private String esbUrl; + + private Preload preload = new Preload(); + + private boolean skipTranslation; + private boolean skipMvvRetrieve; + private boolean skipBrokerCep; + private boolean skipBaguette; + private boolean skipCollectors; + private boolean skipMetasolver; + private boolean skipEsbNotification; + + private String tcLoadFile; + private String tcSaveFile; + + private boolean exitAllowed; + @Min(1) + private long exitGracePeriod = 10; + private int exitCode = 0; + + // control.ssl.** settings + @NestedConfigurationProperty + private KeystoreAndCertificateProperties ssl; + + @Data + public static class Preload { + private String camelModel; + private String cpModel; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/InfoServiceProperties.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/InfoServiceProperties.java new file mode 100644 index 0000000..e2d1c43 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/InfoServiceProperties.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.properties; + +import gr.iccs.imu.ems.control.util.EventBusCache; +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "info") +public class InfoServiceProperties implements InitializingBean { + @Min(1) + private long metricsUpdateInterval = 1000; + @Min(1) + private long metricsClientUpdateInterval = 500; //XXX:TODO: Not really needed since clients PUSH their statistics to server + @Min(1) + private int metricsStreamUpdateInterval = 10; // in seconds + @NotBlank + private String metricsStreamEventName = "ems-metrics-event"; + private List envVarPrefixes = Arrays.asList("WEBSSH_SERVICE_-^", "WEB_ADMIN_!^"); + // ! at the end means to trim off the prefix; - at the end means to convert '_' to '-'; + // ^ at the end means convert to upper case; ~ at the end means convert to lower case; + + private FileExplorerProperties files = new FileExplorerProperties(); + + private List logViewerFiles = Collections.emptyList(); + + private boolean eventBusCacheEnabled = true; + private int eventBusCacheSize = EventBusCache.DEFAULT_EVENT_BUS_CACHE_SIZE; + + @Override + public void afterPropertiesSet() { + log.debug("InfoServiceProperties: {}", this); + } + + @Data + public static class FileExplorerProperties { + private boolean enabled = true; + private List roots = Collections.emptyList(); + private List extensionsBlocked = Collections.emptyList(); + private boolean listBlocked = true; + private boolean listHidden = true; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/StaticResourceProperties.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/StaticResourceProperties.java new file mode 100644 index 0000000..1cea590 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/StaticResourceProperties.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.properties; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "web.static") +public class StaticResourceProperties implements InitializingBean { + @Override + public void afterPropertiesSet() { + log.debug("StaticResourceProperties: {}", this); + } + + /*private String faviconContext = "/favicon.ico"; + private String faviconPath;*/ + + private String resourceContext = "/resources/**"; + private List resourcePath; + + private String logsContext = "/logs/**"; + private List logsPath; + + private String redirect; + private Map redirects = new LinkedHashMap<>(); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/TopicBeaconProperties.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/TopicBeaconProperties.java new file mode 100644 index 0000000..c79b4d1 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/TopicBeaconProperties.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.properties; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Min; +import java.util.HashSet; +import java.util.Set; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "beacon") +public class TopicBeaconProperties implements InitializingBean { + private boolean enabled = true; + @Min(0) private long initialDelay = 60000; + @Min(1) private long delay = 60000; + @Min(1) private long rate = 60000; + private boolean useDelay = true; + + private Set heartbeatTopics = new HashSet<>(); + private Set thresholdTopics = new HashSet<>(); + private Set instanceTopics = new HashSet<>(); + private Set predictionTopics = new HashSet<>(); + @Min(1) private long predictionRate = 60000; + @Min(1) private long predictionMinAllowedRate = 1; + @Min(1) private long predictionMaxAllowedRate = 365*24*3600*1000L; + private Set sloViolationDetectorTopics = new HashSet<>(); + + @Override + public void afterPropertiesSet() { + log.debug("TopicBeaconProperties: {}", this); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/WebSecurityProperties.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/WebSecurityProperties.java new file mode 100644 index 0000000..a4b597d --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/properties/WebSecurityProperties.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.properties; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "web.security") +public class WebSecurityProperties implements InitializingBean { + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("WebSecurityProperties: {}", this); + } + + // JWT Token authentication + @Valid + @NotNull + private JwtAuthentication jwtAuthentication = new JwtAuthentication(); + + @Data + public static class JwtAuthentication { + private boolean enabled = true; + private String requestParameter; + private boolean printSampleToken; + } + + // API-Key authentication + @Valid + @NotNull + private ApiKeyAuthentication apiKeyAuthentication = new ApiKeyAuthentication(); + + @Data + public static class ApiKeyAuthentication { + private boolean enabled = true; + private String requestHeader = "EMS-API-KEY"; + private String requestParameter = "ems-api-key"; + private String value; + } + + // OTP authentication + @Valid + @NotNull + private OtpAuthentication otpAuthentication = new OtpAuthentication(); + + @Data + public static class OtpAuthentication { + private boolean enabled = true; + @Min(1) + private long duration = 3600000; + private String requestHeader = "EMS-OTP"; + private String requestParameter = "ems-otp"; + } + + // User form authentication + @Valid + @NotNull + private FormAuthentication formAuthentication = new FormAuthentication(); + + @Data + public static class FormAuthentication { + private boolean enabled = true; + private String username = "admin"; + private String password; + + private String loginPage = "/admin/login.html"; + private String loginUrl = "/login"; + private String loginSuccessUrl = "/"; + private String loginFailureUrl = "/admin/login.html?error=Invalid+username+or+password"; + private String logoutUrl = "/logout"; + private String logoutSuccessUrl = "/admin/login.html?message=Signed+out"; + } + + // Permitted URLs + private List permittedUrls = Arrays.asList( + "/login*", "/logout*", "/favicon.ico", "/admin/login.html", "/admin/favicon.ico", "/admin/assets/**", "/resources/*"); +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/EventBusCache.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/EventBusCache.java new file mode 100644 index 0000000..576f010 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/EventBusCache.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util; + +import gr.iccs.imu.ems.control.controller.ControlServiceCoordinator; +import gr.iccs.imu.ems.control.properties.InfoServiceProperties; +import gr.iccs.imu.ems.util.EventBus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EventBusCache implements InitializingBean, EventBus.EventConsumer { + public final static int DEFAULT_EVENT_BUS_CACHE_SIZE = 100; + public final static List DEFAULT_TOPICS = Arrays.asList( + ControlServiceCoordinator.COORDINATOR_STATUS_TOPIC + ); + + private final EventBus eventBus; + private final InfoServiceProperties properties; + private final AtomicLong cacheCounter = new AtomicLong(0); + private ArrayBlockingQueue messageCache; + private boolean enabled; + + @Override + public void afterPropertiesSet() throws Exception { + enabled = properties.isEventBusCacheEnabled() && properties.getEventBusCacheSize()!=0; + if (!enabled) return; + + int s = properties.getEventBusCacheSize(); + if (s<0) s = DEFAULT_EVENT_BUS_CACHE_SIZE; + messageCache = new ArrayBlockingQueue<>(s); + + DEFAULT_TOPICS.forEach(topic -> eventBus.subscribe(topic, this)); + } + + public List asList() { + return enabled ? new ArrayList<>(messageCache) : Collections.emptyList(); + } + + public synchronized void clearCache() { + clearCache(false); + } + + public synchronized void clearCache(boolean resetCounter) { + if (!enabled) return; + messageCache.clear(); + cacheCounter.set(0); + } + + public void cacheEvent(String topic, Map message, Object sender) { + if (!enabled) return; + EventBusCache.CacheEntry entry; + synchronized (cacheCounter) { + try { + while (messageCache.remainingCapacity() == 0) + messageCache.poll(); + entry = new EventBusCache.CacheEntry( + topic, message, Map.of("sender", sender), + cacheCounter.getAndIncrement(), + System.currentTimeMillis()); + if (!messageCache.offer(entry)) { + log.warn("EventBusCache.cacheEvent: Failed to cache event. Cache is full: size={}", messageCache.size()); + } + } catch (Throwable e) { + log.warn("EventBusCache.cacheEvent: Exception while caching event: ", e); + } + } + } + + @Override + public void onMessage(String topic, Object message, Object sender) { + if (message instanceof Map m) { + Map map = m.entrySet().stream() + .filter(e -> e.getKey() instanceof String) + .collect(Collectors.toMap( + e -> ((String) e.getKey()), Map.Entry::getValue + )); + cacheEvent(topic, map, sender!=null ? sender.getClass().getSimpleName() : null); + } + } + + @RequiredArgsConstructor + public static class CacheEntry { + public final String destination; + public final Map payload; + public final Map properties; + public final long counter; + public final long timestamp; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/TopicBeacon.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/TopicBeacon.java new file mode 100644 index 0000000..5337658 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/TopicBeacon.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import gr.iccs.imu.ems.baguette.server.NodeRegistryEntry; +import gr.iccs.imu.ems.brokercep.BrokerCepService; +import gr.iccs.imu.ems.brokercep.event.EventMap; +import gr.iccs.imu.ems.control.controller.ControlServiceCoordinator; +import gr.iccs.imu.ems.control.plugin.BeaconPlugin; +import gr.iccs.imu.ems.control.properties.TopicBeaconProperties; +import gr.iccs.imu.ems.translate.TranslationContext; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Service; + +import javax.jms.JMSException; +import java.io.Serializable; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; + +@Slf4j +@Service +@EnableScheduling +@RequiredArgsConstructor +public class TopicBeacon implements InitializingBean { + @Getter + private final TopicBeaconProperties properties; + + private final ControlServiceCoordinator coordinator; + private final BrokerCepService brokerCepService; + private final TaskScheduler scheduler; + private final BeaconContext beaconContext = new BeaconContext(this); + private final List beaconPlugins; + + private Gson gson; + private String previousModelId = ""; + private final AtomicLong modelVersion = new AtomicLong(0); + + @Override + public void afterPropertiesSet() throws Exception { + if (!properties.isEnabled()) { + log.warn("Topic Beacon is disabled"); + return; + } + + // initialize a Gson instance + gson = new GsonBuilder().disableHtmlEscaping().create(); + + // configure and start scheduler + Date startTime = new Date(System.currentTimeMillis() + properties.getInitialDelay()); + log.debug("Topic Beacon settings: init-delay={}, delay={}, heartbeat-topics={}, threshold-topics={}, instance-topics={}", + properties.getInitialDelay(), properties.getDelay(), properties.getHeartbeatTopics(), properties.getThresholdTopics(), + properties.getInstanceTopics()); + + Runnable transmitInfoTask = () -> { + try { + transmitInfo(); + } catch (Exception e) { + log.error("Topic Beacon: Exception while sending info: ", e); + } + }; + if (properties.isUseDelay()) { + scheduler.scheduleWithFixedDelay(transmitInfoTask, startTime.toInstant(), Duration.ofMillis(properties.getDelay())); + log.info("Topic Beacon started: init-delay={}ms, delay={}ms", properties.getInitialDelay(), properties.getDelay()); + } else { + scheduler.scheduleAtFixedRate(transmitInfoTask, startTime.toInstant(), Duration.ofMillis(properties.getRate())); + log.info("Topic Beacon started: init-delay={}ms, rate={}ms", properties.getInitialDelay(), properties.getRate()); + } + } + + private Set emptyIfNull(Set s) { + if (s==null) return Collections.emptySet(); + return s; + } + + public long getModelVersion() { + return modelVersion.get(); + } + + public String toJson(Object o) { + return gson.toJson(o); + } + + public void transmitInfo() throws JMSException { + log.debug("Topic Beacon: Start transmitting info: {}", new Date()); + updateModelVersion(); + + // Call standard transmit methods + transmitHeartbeat(); + transmitThresholdInfo(); + transmitInstanceInfo(); + + // Call Beacon plugins + beaconPlugins.stream().filter(Objects::nonNull).forEach(plugin -> { + try { + log.debug("Topic Beacon: Calling Beacon plugin: {}", plugin.getClass().getName()); + plugin.transmit(beaconContext); + } catch (Throwable t) { + log.error("Topic Beacon: EXCEPTION in Beacon plugin: {}\n", plugin.getClass().getName(), t); + } + }); + + log.debug("Topic Beacon: Completed transmitting info: {}", new Date()); + } + + public void transmitHeartbeat() throws JMSException { + if (emptyIfNull(properties.getHeartbeatTopics()).isEmpty()) return; + + String message = "TOPIC BEACON HEARTBEAT "+new Date(); + log.debug("Topic Beacon: Transmitting Heartbeat info: message={}, topics={}", message, properties.getHeartbeatTopics()); + sendMessageToTopics(message, properties.getHeartbeatTopics()); + } + + public void transmitThresholdInfo() { + if (emptyIfNull(properties.getThresholdTopics()).isEmpty()) return; + + if (coordinator.getTranslationContextOfAppModel(coordinator.getCurrentAppModelId())==null) + return; + coordinator.getTranslationContextOfAppModel(coordinator.getCurrentAppModelId()) + .getMetricConstraints() + .forEach(c -> { + String message = gson.toJson(c); + log.debug("Topic Beacon: Transmitting Metric Constraint threshold info: message={}, topics={}",message, properties.getThresholdTopics()); + try { + sendEventToTopics(message, properties.getThresholdTopics()); + } catch (JMSException e) { + log.error("Topic Beacon: EXCEPTION while transmitting Metric Constraint threshold info: message={}, topics={}, exception: ", + message, properties.getThresholdTopics(), e); + } + }); + } + + public void transmitInstanceInfo() throws JMSException { + if (emptyIfNull(properties.getInstanceTopics()).isEmpty()) return; + + if (coordinator.getBaguetteServer().isServerRunning()) { + log.debug("Topic Beacon: Transmitting Instance info: topics={}", properties.getInstanceTopics()); + for (NodeRegistryEntry node : coordinator.getBaguetteServer().getNodeRegistry().getNodes()) { + String nodeName = node.getPreregistration().getOrDefault("name", ""); + String nodeIp = node.getIpAddress(); + //String nodeIp = node.getPreregistration().getOrDefault("ip",""); + String message = gson.toJson(node); + log.debug("Topic Beacon: Transmitting Instance info for: instance={}, ip-address={}, message={}, topics={}", + nodeName, nodeIp, message, properties.getInstanceTopics()); + sendEventToTopics(message, properties.getInstanceTopics()); + } + } + } + + // ------------------------------------------------------------------------ + + private void sendEventToTopics(String message, Set topics) throws JMSException { + EventMap event = new EventMap(-1); + event.put("message", message); + sendMessageToTopics(event, topics); + } + + private void sendMessageToTopics(Serializable event, Set topics) throws JMSException { + for (String topicName : topics) { + log.trace("Topic Beacon: Sending event to topic: event={}, topic={}", event, topicName); + brokerCepService.publishSerializable( + brokerCepService.getBrokerCepProperties().getBrokerUrlForClients(), + brokerCepService.getBrokerUsername(), + brokerCepService.getBrokerPassword(), + topicName, + event, + false); + log.debug("Topic Beacon: Event sent to topic: event={}, topic={}", event, topicName); + } + } + + private synchronized boolean updateModelVersion() { + String modelId = coordinator.getCurrentAppModelId(); + boolean versionChanged = ! StringUtils.defaultIfBlank(modelId, "").equals(previousModelId); + log.trace("Topic Beacon: updateModelVersion: previousModelId='{}', modelId='{}', version={}, version-changed={}", + previousModelId, modelId, modelVersion.get(), versionChanged); + if (versionChanged) { + long newVersion = modelVersion.incrementAndGet(); + log.info("Topic Beacon: updateModelVersion: Model changed: {} -> {}, version: {}", previousModelId, modelId, newVersion); + previousModelId = modelId; + } + return versionChanged; + } + + @RequiredArgsConstructor + public static class BeaconContext { + @Getter + private final TopicBeacon topicBeacon; + + public TopicBeaconProperties getProperties() { + return topicBeacon.properties; + } + + public String getCurrentAppModelId() { + return topicBeacon.coordinator.getCurrentAppModelId(); + } + + public TranslationContext getTranslationContextOfAppModel(String modelId) { + return topicBeacon.coordinator.getTranslationContextOfAppModel(modelId); + } + + public long getModelVersion() { + return topicBeacon.modelVersion.get(); + } + + public String toJson(Object payload) { + return topicBeacon.toJson(payload); + } + + public void sendEventToTopics(String event, Set topics) throws JMSException { + topicBeacon.sendEventToTopics(event, topics); + } + + public void sendMessageToTopics(Serializable event, Set topics) throws JMSException { + topicBeacon.sendMessageToTopics(event, topics); + } + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/TranslationContextMonitorGsonDeserializer.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/TranslationContextMonitorGsonDeserializer.java new file mode 100644 index 0000000..aeb7b5a --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/TranslationContextMonitorGsonDeserializer.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util; + +import com.google.gson.*; +import gr.iccs.imu.ems.translate.model.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class TranslationContextMonitorGsonDeserializer implements JsonDeserializer { + @Override + public Monitor deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + log.debug("TranslationContextMonitorGsonDeserializer: INPUT: jsonElement={}, type={}, context={}", jsonElement, type, jsonDeserializationContext); + + JsonObject jsonObject = (JsonObject) jsonElement; + Monitor monitor = new Monitor(); + + String metricName = jsonObject.getAsJsonPrimitive("metric").getAsString(); + monitor.setMetric(metricName); + + String _componentName = null; + if (jsonObject.has("component")) { + JsonPrimitive compNameElem = jsonObject.getAsJsonPrimitive("component"); + _componentName = compNameElem!=null ? compNameElem.getAsString() : null; + monitor.setComponent(_componentName); + } + final String componentName = _componentName; + + // Find and initialize sensor + JsonObject jsonSensorObject = jsonObject.getAsJsonObject("sensor"); + if (jsonSensorObject.has("anyType") && jsonSensorObject.get("anyType").isJsonObject()) { + jsonSensorObject = jsonSensorObject.getAsJsonObject("anyType"); + } + + boolean isPull = jsonSensorObject.has("className") + || jsonSensorObject.has("configuration") + || jsonSensorObject.has("interval"); + boolean isPush = jsonSensorObject.has("port"); + if (isPull && isPush) + throw new JsonParseException("Monitor.Sensor contains fields of both PullSensor and PushSensor class: " + + "metric=" + metricName + ", component=" + componentName); + if (!isPull && !isPush) + throw new JsonParseException("Monitor.Sensor contain no fields of either PullSensor or PushSensor class: " + + "metric=" + metricName + ", component=" + componentName); + + Sensor sensor; + if (isPull) { + PullSensor pullSensor = new PullSensor(); + if (jsonSensorObject.has("className") && !jsonSensorObject.get("className").isJsonNull()) { + JsonPrimitive classNameElem = jsonSensorObject.getAsJsonPrimitive("className"); + String className = classNameElem!=null ? classNameElem.getAsString() : null; + pullSensor.setClassName(className); + } + + pullSensor.setConfiguration( getConfiguration(jsonSensorObject, metricName, componentName, "PullSensor") ); + + pullSensor.setInterval( getInterval(jsonSensorObject, metricName, componentName, "PullSensor") ); + + sensor = pullSensor; + } else if (isPush) { + PushSensor pushSensor = new PushSensor(); + int port = jsonSensorObject.getAsJsonPrimitive("port").getAsInt(); + pushSensor.setPort(port); + sensor = pushSensor; + } else { + throw new JsonParseException("Monitor.Sensor is neither Pull or Push: " + + "jsonSensorObject=" + jsonSensorObject); + } + monitor.setSensor(sensor); + + // Get sinks + if (jsonObject.has("sinks")) { + if (!jsonObject.get("sinks").isJsonNull()) { + if (!jsonObject.get("sinks").isJsonArray()) + throw new JsonParseException("Monitor.sinks must be an array or null: " + + "metric=" + metricName + ", component=" + componentName); + + List sinks = new ArrayList<>(); + JsonArray jsonSinkArray = jsonObject.getAsJsonArray("sinks"); + jsonSinkArray.forEach(elem -> { + if (!elem.isJsonNull()) { + JsonObject jsonSinkElem = elem.getAsJsonObject(); + Sink sink = new Sink(); + sink.setType(Sink.Type.valueOf(jsonSinkElem.getAsJsonPrimitive("type").getAsString())); + sink.setConfiguration(getConfiguration(jsonSinkElem, metricName, componentName, "PullSensor.sinks[]")); + sinks.add(sink); + } + }); + + monitor.setSinks(sinks); + } + } + + log.debug("TranslationContextMonitorGsonDeserializer: OUTPUT: monitor={}", monitor); + return monitor; + } + + public Map getConfiguration(JsonObject jsonObject, String metricName, String componentName, String field) { + if (!jsonObject.has("configuration")) return null; + if (jsonObject.get("configuration").isJsonNull()) return null; + if (!jsonObject.get("configuration").isJsonObject()) + throw new JsonParseException("Monitor."+field+".configuration must be a map or null: " + + "metric=" + metricName + ", component=" + componentName); + + Map configPairs = new LinkedHashMap<>(); + JsonObject jsonConfigMap = jsonObject.getAsJsonObject("configuration"); + jsonConfigMap.entrySet().forEach(entry -> { + String key = entry.getKey(); + String val = null; + JsonElement jsonElem = entry.getValue(); + + if (StringUtils.isBlank(key)) + throw new JsonParseException("Monitor."+field+".configuration contains a blank key: " + + "metric=" + metricName + ", component=" + componentName); + + if (jsonElem.isJsonNull()) + val = null; + else if (jsonElem.isJsonPrimitive()) + val = jsonElem.getAsString(); + else + throw new JsonParseException("Monitor."+field+".configuration entry contains a non-string value: " + + "metric=" + metricName + ", component=" + componentName + ", configuration[].key=" + key); + + configPairs.put(key, val); + }); + + return configPairs; + } + + public Interval getInterval(JsonObject jsonObject, String metricName, String componentName, String field) { + if (!jsonObject.has("interval")) return null; + if (jsonObject.get("interval").isJsonNull()) return null; + if (!jsonObject.get("interval").isJsonObject()) + throw new JsonParseException("Monitor.Sensor."+field+".interval must be an object or null: " + + "metric=" + metricName + ", component=" + componentName); + + JsonObject jsonIntervalObject = jsonObject.getAsJsonObject("interval"); + JsonElement unitElem = jsonIntervalObject.get("unit"); + JsonElement periodElem = jsonIntervalObject.get("period"); + + if (unitElem.isJsonNull()) + throw new JsonParseException("Monitor."+field+".interval.unit cannot be null: " + + "metric=" + metricName + ", component=" + componentName); + if (!unitElem.isJsonPrimitive()) + throw new JsonParseException("Monitor."+field+".interval.unit must be a member of Interval.UnitType enum: " + + "metric=" + metricName + ", component=" + componentName); + + if (periodElem.isJsonNull()) + throw new JsonParseException("Monitor."+field+".interval.period cannot be null: " + + "metric=" + metricName + ", component=" + componentName); + if (!periodElem.isJsonPrimitive()) + throw new JsonParseException("Monitor."+field+".interval.period must be an integer: " + + "metric=" + metricName + ", component=" + componentName); + + Interval interval = new Interval(); + interval.setUnit(Interval.UnitType.valueOf(unitElem.getAsString())); + interval.setPeriod(periodElem.getAsInt()); + return interval; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/WebClientUtil.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/WebClientUtil.java new file mode 100644 index 0000000..0d3d44a --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/WebClientUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util; + +import gr.iccs.imu.ems.util.KeystoreAndCertificateProperties; +import gr.iccs.imu.ems.util.KeystoreUtil; +import io.netty.handler.ssl.SslContextBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +@Slf4j +public class WebClientUtil { + public WebClient createInstance(KeystoreAndCertificateProperties sslProperties) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + // Load keystore and truststore + KeyStore keyStore = KeystoreUtil.readKeystore( + sslProperties.getKeystoreFile(), + sslProperties.getKeystoreType(), + sslProperties.getKeystorePassword()); + KeyStore trustStore = KeystoreUtil.readKeystore( + sslProperties.getTruststoreFile(), + sslProperties.getTruststoreType(), + sslProperties.getTruststorePassword()); + + // Create and initialize keystore and truststore managers + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, sslProperties.getKeystorePassword().toCharArray()); + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + // Create an SSL context and an HTTP client + io.netty.handler.ssl.SslContext sslContext = SslContextBuilder.forClient() + .keyManager(keyManagerFactory) + .trustManager(trustManagerFactory) + //.trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + HttpClient httpClient = HttpClient.create() + .secure(sslSpec -> sslSpec.sslContext(sslContext)); + + // Create and return a WebClient (WebFlux) instance + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenProperties.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenProperties.java new file mode 100644 index 0000000..ba95937 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util.jwt; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "jwt") +public class JwtTokenProperties implements InitializingBean { + @Override + public void afterPropertiesSet() throws Exception { + log.debug("JwtTokenProperties: {}", this); + } + + @NotBlank + @ToString.Exclude + private String secret; + @NotNull + private Long expirationTime = Duration.ofDays(1).toMillis(); + @NotNull + private Long refreshTokenExpirationTime = Duration.ofDays(1).toMillis(); +} \ No newline at end of file diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenService.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenService.java new file mode 100644 index 0000000..1b876e3 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenService.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util.jwt; + +import gr.iccs.imu.ems.util.PasswordUtil; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.*; + +@Slf4j +@Service +@AllArgsConstructor +public class JwtTokenService { + + public static final String TOKEN_PREFIX = "Bearer "; + public static final String HEADER_STRING = "Authorization"; + public static final String REFRESH_HEADER_STRING = "Refresh"; + public static final String AUDIENCE_UPPERWARE = "UPPERWARE"; + public static final String AUDIENCE_JWT = "JWT_SERVER"; + + private JwtTokenProperties jwtTokenProperties; + private PasswordUtil passwordUtil; + + // ------------------------------------------------------------------------ + // Key-related methods + // ------------------------------------------------------------------------ + + public Key createKey() { + Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); + log.debug("JwtTokenService.createKey(): algorithm={}, format={}, key-size={}, base64-encoded-key={}", + key.getAlgorithm(), key.getFormat(), key.getEncoded().length, passwordUtil.encodePassword(keyToString(key))); + return key; + } + + @SneakyThrows + protected Key getKeyFromProperties() { + if (StringUtils.isBlank(jwtTokenProperties.getSecret())) + throw new InvalidPropertiesFormatException("JWT token secret key is blank. Check 'jwt.secret' property."); + Key key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtTokenProperties.getSecret())); + log.debug("JwtTokenService.getKeyFromProperties(): algorithm={}, format={}, key-size={}, base64-encoded-key={}", + key.getAlgorithm(), key.getFormat(), key.getEncoded().length, passwordUtil.encodePassword(keyToString(key))); + return key; + } + + protected String keyToString(Key key) { + return Base64.getEncoder().encodeToString(key.getEncoded()); + } + + // ------------------------------------------------------------------------ + // JWT-related methods + // ------------------------------------------------------------------------ + + public Claims parseToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getKeyFromProperties()) + .build() + .parseClaimsJws(token.replace(TOKEN_PREFIX, "")) + .getBody(); + } + + public String createToken(String userName) { + return createToken(userName, getKeyFromProperties()); + } + + public String createToken(String userName, Key key) { + return Jwts.builder() + .setSubject(userName) + .setAudience(AUDIENCE_UPPERWARE) + .setExpiration(new Date(System.currentTimeMillis() + jwtTokenProperties.getExpirationTime())) + .signWith(key) + .compact(); + } + + public String createRefreshToken(String userName) { + Map header = new HashMap<>(); + header.put(Header.CONTENT_TYPE, REFRESH_HEADER_STRING); + return Jwts.builder() + .setSubject(userName) + .setHeader(header) + .setAudience(AUDIENCE_JWT) + .setId(UUID.randomUUID().toString()) + .setExpiration(new Date(System.currentTimeMillis() + jwtTokenProperties.getRefreshTokenExpirationTime())) + .signWith(getKeyFromProperties()) + .compact(); + } +} \ No newline at end of file diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenUtil.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenUtil.java new file mode 100644 index 0000000..6a1c559 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/jwt/JwtTokenUtil.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util.jwt; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +/** + * Run: + * java -cp .\target\control-service.jar -Dloader.main=jwt.util.gr.iccs.imu.ems.control.JwtTokenUtil -Dlogging.level.ROOT=WARN -Dlogging.level.gr.iccs.imu.ems.util=ERROR org.springframework.boot.loader.PropertiesLauncher createKey + * -or- + * java -cp .\target\control-service.jar -Dloader.main=jwt.util.gr.iccs.imu.ems.control.JwtTokenUtil -Dlogging.level.ROOT=WARN -Dlogging.level.gr.iccs.imu.ems.util=ERROR org.springframework.boot.loader.PropertiesLauncher create [USER]? + * -or- + * java -cp .\target\control-service.jar -Dloader.main=jwt.util.gr.iccs.imu.ems.control.JwtTokenUtil -Dlogging.level.ROOT=WARN -Dlogging.level.gr.iccs.imu.ems.util=ERROR org.springframework.boot.loader.PropertiesLauncher parser [TOKEN] + */ +@Slf4j +@SpringBootApplication +@ComponentScan(basePackages = { "gr.iccs.imu.ems.control.util.jwt", "gr.iccs.imu.ems.util", "com.ulisesbocchio" }) +@RequiredArgsConstructor +public class JwtTokenUtil { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(JwtTokenUtil.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.setWebApplicationType(WebApplicationType.NONE); + springApplication.setLogStartupInfo(false); + ConfigurableApplicationContext ctx = springApplication.run(args); + + try { + execCommand(ctx.getBean(JwtTokenService.class), args); + } catch (Exception e) { + System.err.printf("%sERROR: %s%s\n", ConsoleColors.RED_BOLD_BRIGHT, getExceptionMessages(e), ConsoleColors.RESET); + exit(1); + } + } + + public static void execCommand(JwtTokenService jwtService, String... args) { + if (args.length>0) { + String token; + if ("createKey".equalsIgnoreCase(args[0].trim())) { + String key = jwtService.keyToString(jwtService.createKey()); + System.out.printf("%sNew secret key:\n%s%s%s\n", ConsoleColors.WHITE_BOLD_BRIGHT, ConsoleColors.YELLOW_BOLD_BRIGHT, key, ConsoleColors.RESET); + } else if ("create".equalsIgnoreCase(args[0].trim())) { + String user = args.length > 1 && !args[1].trim().isEmpty() ? args[1].trim() : "USER"; + token = jwtService.createToken(user); + System.out.printf("%sNew JWT token for user %s%s:\n%s%s%s\n", + ConsoleColors.GREEN_BOLD_BRIGHT, ConsoleColors.WHITE_BOLD_BRIGHT, user, ConsoleColors.CYAN_BOLD_BRIGHT, token, ConsoleColors.RESET); + } else if ("parse".equalsIgnoreCase(args[0].trim())) { + token = args[1]; + try { + Claims claims = jwtService.parseToken(token); + System.out.printf("%sToken claims: %s %s%s\n", ConsoleColors.GREEN_BOLD_BRIGHT, ConsoleColors.CYAN_BOLD_BRIGHT, claims, ConsoleColors.RESET); + } catch (Exception e) { + System.err.printf("%s%s%s\n", ConsoleColors.RED_BOLD_BRIGHT, getExceptionMessages(e), ConsoleColors.RESET); + exit(2); + } + } else { + System.err.printf("%sUnknown command: %s %s %s\n", ConsoleColors.RED_BOLD_BRIGHT, ConsoleColors.RED_BACKGROUND+ConsoleColors.YELLOW_BOLD_BRIGHT, args[0], ConsoleColors.RESET); + exit(3); + } + } else { + System.err.printf("%sNo command specified%s\n", ConsoleColors.RED_BOLD_BRIGHT, ConsoleColors.RESET); + exit(4); + } + } + + private static String getExceptionMessages(Exception e) { + StringBuilder s = new StringBuilder(); + s.append(e.getMessage()); + Throwable t = e.getCause(); + while (t!=null) { s.append(" -> ").append(t.getMessage()); t = t.getCause(); } + return s.toString(); + } + + protected static void exit(int errorCode) { + System.exit(errorCode); + } + + // See: https://stackoverflow.com/questions/5762491/how-to-print-color-in-console-using-system-out-println + public static class ConsoleColors { + // Reset + public static final String RESET = "\033[0m"; // Text Reset + + // Regular Colors + public static final String BLACK = "\033[0;30m"; // BLACK + public static final String RED = "\033[0;31m"; // RED + public static final String GREEN = "\033[0;32m"; // GREEN + public static final String YELLOW = "\033[0;33m"; // YELLOW + public static final String BLUE = "\033[0;34m"; // BLUE + public static final String PURPLE = "\033[0;35m"; // PURPLE + public static final String CYAN = "\033[0;36m"; // CYAN + public static final String WHITE = "\033[0;37m"; // WHITE + + // Bold + public static final String BLACK_BOLD = "\033[1;30m"; // BLACK + public static final String RED_BOLD = "\033[1;31m"; // RED + public static final String GREEN_BOLD = "\033[1;32m"; // GREEN + public static final String YELLOW_BOLD = "\033[1;33m"; // YELLOW + public static final String BLUE_BOLD = "\033[1;34m"; // BLUE + public static final String PURPLE_BOLD = "\033[1;35m"; // PURPLE + public static final String CYAN_BOLD = "\033[1;36m"; // CYAN + public static final String WHITE_BOLD = "\033[1;37m"; // WHITE + + // Underline + public static final String BLACK_UNDERLINED = "\033[4;30m"; // BLACK + public static final String RED_UNDERLINED = "\033[4;31m"; // RED + public static final String GREEN_UNDERLINED = "\033[4;32m"; // GREEN + public static final String YELLOW_UNDERLINED = "\033[4;33m"; // YELLOW + public static final String BLUE_UNDERLINED = "\033[4;34m"; // BLUE + public static final String PURPLE_UNDERLINED = "\033[4;35m"; // PURPLE + public static final String CYAN_UNDERLINED = "\033[4;36m"; // CYAN + public static final String WHITE_UNDERLINED = "\033[4;37m"; // WHITE + + // Background + public static final String BLACK_BACKGROUND = "\033[40m"; // BLACK + public static final String RED_BACKGROUND = "\033[41m"; // RED + public static final String GREEN_BACKGROUND = "\033[42m"; // GREEN + public static final String YELLOW_BACKGROUND = "\033[43m"; // YELLOW + public static final String BLUE_BACKGROUND = "\033[44m"; // BLUE + public static final String PURPLE_BACKGROUND = "\033[45m"; // PURPLE + public static final String CYAN_BACKGROUND = "\033[46m"; // CYAN + public static final String WHITE_BACKGROUND = "\033[47m"; // WHITE + + // High Intensity + public static final String BLACK_BRIGHT = "\033[0;90m"; // BLACK + public static final String RED_BRIGHT = "\033[0;91m"; // RED + public static final String GREEN_BRIGHT = "\033[0;92m"; // GREEN + public static final String YELLOW_BRIGHT = "\033[0;93m"; // YELLOW + public static final String BLUE_BRIGHT = "\033[0;94m"; // BLUE + public static final String PURPLE_BRIGHT = "\033[0;95m"; // PURPLE + public static final String CYAN_BRIGHT = "\033[0;96m"; // CYAN + public static final String WHITE_BRIGHT = "\033[0;97m"; // WHITE + + // Bold High Intensity + public static final String BLACK_BOLD_BRIGHT = "\033[1;90m"; // BLACK + public static final String RED_BOLD_BRIGHT = "\033[1;91m"; // RED + public static final String GREEN_BOLD_BRIGHT = "\033[1;92m"; // GREEN + public static final String YELLOW_BOLD_BRIGHT = "\033[1;93m";// YELLOW + public static final String BLUE_BOLD_BRIGHT = "\033[1;94m"; // BLUE + public static final String PURPLE_BOLD_BRIGHT = "\033[1;95m";// PURPLE + public static final String CYAN_BOLD_BRIGHT = "\033[1;96m"; // CYAN + public static final String WHITE_BOLD_BRIGHT = "\033[1;97m"; // WHITE + + // High Intensity backgrounds + public static final String BLACK_BACKGROUND_BRIGHT = "\033[0;100m";// BLACK + public static final String RED_BACKGROUND_BRIGHT = "\033[0;101m";// RED + public static final String GREEN_BACKGROUND_BRIGHT = "\033[0;102m";// GREEN + public static final String YELLOW_BACKGROUND_BRIGHT = "\033[0;103m";// YELLOW + public static final String BLUE_BACKGROUND_BRIGHT = "\033[0;104m";// BLUE + public static final String PURPLE_BACKGROUND_BRIGHT = "\033[0;105m"; // PURPLE + public static final String CYAN_BACKGROUND_BRIGHT = "\033[0;106m"; // CYAN + public static final String WHITE_BACKGROUND_BRIGHT = "\033[0;107m"; // WHITE + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/mvv/NoopMetricVariableValuesServiceImpl.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/mvv/NoopMetricVariableValuesServiceImpl.java new file mode 100644 index 0000000..2f265a9 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/util/mvv/NoopMetricVariableValuesServiceImpl.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.util.mvv; + +import gr.iccs.imu.ems.translate.TranslationContext; +import gr.iccs.imu.ems.translate.mvv.MetricVariableValuesService; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +@Service +public class NoopMetricVariableValuesServiceImpl implements MetricVariableValuesService { + public void init() { } + + @SneakyThrows + public Map getMatchingMetricVariableValues(String cpModelPath, TranslationContext _TC) { + return Collections.emptyMap(); + } + + @SneakyThrows + public Map getMetricVariableValues(String cpModelPath, Set variableNames) { + return Collections.emptyMap(); + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/StaticResourceConfiguration.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/StaticResourceConfiguration.java new file mode 100644 index 0000000..7a09073 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/StaticResourceConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.webconf; + +import gr.iccs.imu.ems.control.properties.StaticResourceProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class StaticResourceConfiguration implements WebMvcConfigurer, InitializingBean { + private final StaticResourceProperties properties; + + public void afterPropertiesSet() { + log.debug("StaticResourceConfiguration: afterPropertiesSet: {}", properties); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + /*String faviconContext = properties.getFaviconContext(); + String faviconPath = properties.getFaviconPath(); + if(StringUtils.isNotBlank(faviconPath)) { + log.debug("Serving favicon.ico from: {} --> {}", faviconContext, faviconPath); + registry + .addResourceHandler(faviconContext) + .addResourceLocations(faviconPath); + }*/ + + String resourceContext = properties.getResourceContext(); + List resourcePath = properties.getResourcePath(); + if (resourcePath != null && resourcePath.size() > 0) { + log.debug("Serving static content from: {} --> {}", resourceContext, resourcePath); + registry + .addResourceHandler(resourceContext) + .addResourceLocations(resourcePath.toArray(new String[0])); + } + + String logsContext = properties.getLogsContext(); + List logsPath = properties.getLogsPath(); + if (logsPath != null && logsPath.size() > 0) { + log.debug("Serving logs from: {} --> {}", logsContext, logsPath); + registry + .addResourceHandler(logsContext) + .addResourceLocations(logsPath.toArray(new String[0])); + } + + WebMvcConfigurer.super.addResourceHandlers(registry); + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // Remains for backward compatibility (of properties file) + String resourceRedirect = properties.getRedirect(); + if (StringUtils.isNotBlank(resourceRedirect)) { + log.debug("Redirecting / to: {}", resourceRedirect); + registry + .addViewController("/") + .setViewName("redirect:" + resourceRedirect); + } + + Map resourceRedirects = properties.getRedirects(); + log.debug("Configured resource redirects: {}", resourceRedirects); + if (resourceRedirects!=null) { + resourceRedirects.forEach((context, redirect) -> { + if (StringUtils.isNotBlank(context) && StringUtils.isNotBlank(redirect)) { + context = context.trim(); + redirect = redirect.trim(); + log.debug("Redirecting {} to: {}", context, redirect); + registry + .addViewController(context) + .setViewName("redirect:" + redirect); + } + }); + } + + WebMvcConfigurer.super.addViewControllers(registry); + } + + @ConditionalOnProperty(name="control.log-requests", matchIfMissing = true) + @Bean + public CommonsRequestLoggingFilter logFilter() { + CommonsRequestLoggingFilter filter + = new CommonsRequestLoggingFilter(); + filter.setIncludeQueryString(true); + filter.setIncludePayload(true); + filter.setMaxPayloadLength(10000); + filter.setIncludeHeaders(true); + filter.setIncludeClientInfo(true); + + filter.setBeforeMessagePrefix("REQUEST DATA BEFORE: >>"); + filter.setBeforeMessageSuffix("<< REQUEST DATA BEFORE"); + filter.setAfterMessagePrefix("REQUEST DATA AFTER: >>"); + filter.setAfterMessageSuffix("<< REQUEST DATA AFTER"); + return filter; + } +} diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/WebMvcConfig.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/WebMvcConfig.java new file mode 100644 index 0000000..9be2504 --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/WebMvcConfig.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.webconf; + +import jakarta.servlet.Filter; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + private final ApplicationContext applicationContext; + + @Override + public void configureAsyncSupport(AsyncSupportConfigurer configurer) { + configurer.setTaskExecutor(applicationContext.getBean("asyncExecutor", AsyncTaskExecutor.class)); + } + + @Bean(name="asyncExecutor") + public AsyncTaskExecutor asyncExecutor() { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(); + log.debug("asyncExecutor(): ThreadPoolExecutor: core={}, max={}, size={}, active={}, keep-alive={}", + executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getPoolSize(), + executor.getActiveCount(), executor.getKeepAliveTime(TimeUnit.SECONDS)); + return new ConcurrentTaskExecutor(executor); + } + + @Bean + public Filter contentCachingFilter() { + log.debug("contentCachingFilter(): Registering content caching request filter"); + return (servletRequest, servletResponse, filterChain) -> { + log.trace("contentCachingFilter(): request={}", servletRequest); + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + //HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + ServletRequest contentCachingRequestWrapper = new ContentCachingRequestWrapper(httpRequest); + //ServletResponse contentCachingResponseWrapper = new ContentCachingResponseWrapper(httpResponse); + log.trace("contentCachingFilter(): request={}, content-caching-request={}", servletRequest, contentCachingRequestWrapper); + //log.trace("contentCachingFilter(): response={}, content-caching-response={}", servletResponse, contentCachingResponseWrapper); + + filterChain.doFilter(contentCachingRequestWrapper, servletResponse); + //filterChain.doFilter(contentCachingRequestWrapper, contentCachingResponseWrapper); + }; + } +} \ No newline at end of file diff --git a/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/WebSecurityConfig.java b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/WebSecurityConfig.java new file mode 100644 index 0000000..c16bb6c --- /dev/null +++ b/ems-core/control-service/src/main/java/gr/iccs/imu/ems/control/webconf/WebSecurityConfig.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.control.webconf; + +//import properties.gr.iccs.imu.ems.control.StaticResourceProperties; +import gr.iccs.imu.ems.control.properties.WebSecurityProperties; +import gr.iccs.imu.ems.control.util.jwt.JwtTokenService; +import gr.iccs.imu.ems.util.PasswordUtil; +import gr.iccs.imu.ems.util.StrUtil; +import io.jsonwebtoken.Claims; +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.security.InvalidParameterException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Order(1) +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class WebSecurityConfig implements InitializingBean { + + public final static String ROLE_USER_FORM = "ROLE_USER_FORM"; + public static final String ROLE_JWT_TOKEN = "ROLE_JWT_TOKEN"; + public static final String ROLE_API_KEY = "ROLE_API_KEY"; + public static final String ROLE_OTP = "ROLE_OTP"; + + //private final StaticResourceProperties staticResourceProperties; + private final WebSecurityProperties properties; + private final PasswordUtil passwordUtil; + private final JwtTokenService jwtTokenService; + + private final Map otpCache = new HashMap<>(); + + @Value("${melodic.security.enabled:true}") + private boolean securityEnabled; + private boolean propertiesCopied; + + // JWT Token authentication fields + private boolean jwtAuthEnabled; + private String jwtRequestParam; + private boolean printSampleJwt; + + // API-Key authentication fields + private boolean apiKeyAuthEnabled; + private String apiKeyRequestHeader; + private String apiKeyRequestParam; + private String apiKeyValue; + + // OTP authentication fields + private boolean otpAuthEnabled; + private long otpDuration; + private String otpRequestHeader; + private String otpRequestParam; + + // User form authentication fields + private boolean userFormAuthEnabled; + private String username; + private String password; + + private String loginPage; + private String loginUrl; + private String loginSuccessUrl; + private String loginFailureUrl; + private String logoutUrl; + private String logoutSuccessUrl; + + // Permitted URLs + private String[] permittedUrls; + + private final static String divider = "--------------------------------------------------------------------------------"; + + @Override + public void afterPropertiesSet() { + copyPropertiesToLocalFields(); + } + + private void copyPropertiesToLocalFields() { + if (properties==null) return; + if (propertiesCopied) return; + + // JWT Token authentication fields + jwtAuthEnabled = properties.getJwtAuthentication().isEnabled(); + jwtRequestParam = properties.getJwtAuthentication().getRequestParameter(); + printSampleJwt = properties.getJwtAuthentication().isPrintSampleToken(); + + // API-Key authentication fields + apiKeyAuthEnabled = properties.getApiKeyAuthentication().isEnabled(); + apiKeyRequestHeader = properties.getApiKeyAuthentication().getRequestHeader(); + apiKeyRequestParam = properties.getApiKeyAuthentication().getRequestParameter(); + apiKeyValue = properties.getApiKeyAuthentication().getValue(); + + // OTP authentication fields + otpAuthEnabled = properties.getOtpAuthentication().isEnabled(); + otpDuration = properties.getOtpAuthentication().getDuration(); + otpRequestHeader = properties.getOtpAuthentication().getRequestHeader(); + otpRequestParam = properties.getOtpAuthentication().getRequestParameter(); + + // User form authentication fields + userFormAuthEnabled = properties.getFormAuthentication().isEnabled(); + username = properties.getFormAuthentication().getUsername(); + password = properties.getFormAuthentication().getPassword(); + + loginPage = properties.getFormAuthentication().getLoginPage(); + loginUrl = properties.getFormAuthentication().getLoginUrl(); + loginSuccessUrl = properties.getFormAuthentication().getLoginSuccessUrl(); + loginFailureUrl = properties.getFormAuthentication().getLoginFailureUrl(); + logoutUrl = properties.getFormAuthentication().getLogoutUrl(); + logoutSuccessUrl = properties.getFormAuthentication().getLogoutSuccessUrl(); + + // Permitted URLs + permittedUrls = properties.getPermittedUrls()!=null + ? properties.getPermittedUrls().toArray(new String[0]) + : new String[0]; + + propertiesCopied = true; + } + + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { + if (securityEnabled && userFormAuthEnabled && (StringUtils.isBlank(username) || StringUtils.isEmpty(password))) + throw new InvalidParameterException("User form authentication is enabled but username or password is blank"); + if (securityEnabled && apiKeyAuthEnabled && StringUtils.isBlank(apiKeyValue)) + throw new InvalidParameterException("API Key authentication is enabled but no API Key provided or it is blank"); + if (permittedUrls==null) permittedUrls = new String[0]; + + if (securityEnabled && userFormAuthEnabled) { + log.debug("afterPropertiesSet: Admin Username: {}", username); + log.debug("afterPropertiesSet: Admin Password: {}", passwordUtil.encodePassword(password)); + } + if (securityEnabled && apiKeyAuthEnabled) { + log.debug("afterPropertiesSet: API Key: {}", passwordUtil.encodePassword(apiKeyValue)); + } + if (printSampleJwt) { + try { + log.info("afterPropertiesSet:\n{}\nSample JWT Token: \nBearer {}\n{}", + divider, jwtTokenService.createToken("USER"), divider); + } catch (Throwable e) { + String s = StrUtil.exceptionToDetailsString(e); + log.error("afterPropertiesSet: Failed to generate sample JWT Token: {}", s); + log.debug("afterPropertiesSet: Failed to generate sample JWT Token: EXCEPTION:\n", e); + } + } + + log.debug("afterPropertiesSet: ---------------------"); + log.debug("afterPropertiesSet: securityEnabled: {}", securityEnabled); + log.debug("afterPropertiesSet: ---------------------"); + log.debug("afterPropertiesSet: jwtTokenAuthEnabled: {}", jwtAuthEnabled); + log.debug("afterPropertiesSet: jwtTokenRequestParam: {}", jwtRequestParam); + log.debug("afterPropertiesSet: ---------------------"); + log.debug("afterPropertiesSet: apiKeyAuthEnabled: {}", apiKeyAuthEnabled); + log.debug("afterPropertiesSet: apiKeyRequestHeader: {}", apiKeyRequestHeader); + log.debug("afterPropertiesSet: apiKeyRequestParam: {}", apiKeyRequestParam); + log.debug("afterPropertiesSet: ---------------------"); + log.debug("afterPropertiesSet: otpAuthEnabled: {}", otpAuthEnabled); + log.debug("afterPropertiesSet: otpDuration: {}", otpDuration); + log.debug("afterPropertiesSet: otpRequestHeader: {}", otpRequestHeader); + log.debug("afterPropertiesSet: otpRequestParam: {}", otpRequestParam); + log.debug("afterPropertiesSet: ---------------------"); + log.debug("afterPropertiesSet: userFormAuthEnabled: {}", userFormAuthEnabled); + log.debug("afterPropertiesSet: username: {}", username); + log.debug("afterPropertiesSet: loginPage: {}", loginPage); + log.debug("afterPropertiesSet: loginUrl: {}", loginUrl); + log.debug("afterPropertiesSet: loginSuccessUrl: {}", loginSuccessUrl); + log.debug("afterPropertiesSet: loginFailUrl: {}", loginFailureUrl); + log.debug("afterPropertiesSet: logoutUrl: {}", logoutUrl); + log.debug("afterPropertiesSet: logoutSuccessUrl: {}", logoutSuccessUrl); + log.debug("afterPropertiesSet: ---------------------"); + log.debug("afterPropertiesSet: permittedUrls: {}", Arrays.asList(permittedUrls)); + log.debug("afterPropertiesSet: ---------------------"); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + copyPropertiesToLocalFields(); + UserDetails userDetails; + if (this.userFormAuthEnabled && StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) { + userDetails = User.builder() + .username(username) + .password(passwordEncoder().encode(password)) + .authorities(ROLE_USER_FORM) + .build(); + log.debug("WebSecurityConfig: User Form Admin credentials have been set: username={}", username); + } else { + userDetails = User.builder().build(); + log.warn("WebSecurityConfig: No Form Admin credentials provided"); + } + return new InMemoryUserDetailsManager(userDetails); + } + + @Bean + public static PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /*@Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring() + // Spring Security should completely ignore the following URLs + .antMatchers(staticResourceProperties.getFaviconContext(), "/health"); + }*/ + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + + // Check configuration settings + checkSettings(); + + // Check if authentication is disabled + log.debug("WebSecurityConfig: security-enabled={}, user-form-auth-enabled={}, jwt-token-auth-enabled={}, api-key-auth-enabled={}, otp-auth-enabled={}", + securityEnabled, userFormAuthEnabled, jwtAuthEnabled, apiKeyAuthEnabled, otpAuthEnabled); + if (!securityEnabled || !userFormAuthEnabled && !jwtAuthEnabled && !apiKeyAuthEnabled && !otpAuthEnabled) { + log.warn("WebSecurityConfig: Authentication is disabled"); + // Authorize all requests + httpSecurity + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + authorize -> authorize.anyRequest().permitAll()); + return httpSecurity.build(); + } + + // Common security settings + httpSecurity + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)); + + // Add and Configure User Form authentication + if (userFormAuthEnabled) { + log.debug("WebSecurityConfig: User form Authentication is enabled"); + httpSecurity + .formLogin(formLogin -> formLogin + .loginPage(loginPage).permitAll() + .loginProcessingUrl(loginUrl).permitAll() + .defaultSuccessUrl(loginSuccessUrl, false) + .failureUrl(loginFailureUrl).permitAll() + ) + .logout(logout -> logout + .logoutUrl(logoutUrl).permitAll() + .logoutSuccessUrl(logoutSuccessUrl).permitAll() + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID") + ); + log.debug("WebSecurityConfig: User form Authentication has been configured"); + } + + // Add configured authentication filters + Class lastAuthFilterClass = UsernamePasswordAuthenticationFilter.class; + Filter f; + if (apiKeyAuthEnabled) { + log.debug("WebSecurityConfig: API-Key Authentication is enabled"); + httpSecurity + .addFilterAfter(f=apiKeyAuthenticationFilter(), lastAuthFilterClass); + lastAuthFilterClass = f.getClass(); + log.debug("WebSecurityConfig: API-Key Authentication filter added"); + } + if (jwtAuthEnabled) { + log.debug("WebSecurityConfig: JWT-Token Authentication is enabled"); + httpSecurity + .addFilterAfter(f=jwtAuthorizationFilter(), lastAuthFilterClass); + lastAuthFilterClass = f.getClass(); + log.debug("WebSecurityConfig: JWT-Token Authentication filter added"); + } + if (otpAuthEnabled) { + log.debug("WebSecurityConfig: OTP Authentication is enabled"); + httpSecurity + .addFilterAfter(f=otpAuthenticationFilter(), lastAuthFilterClass); + lastAuthFilterClass = f.getClass(); + log.debug("WebSecurityConfig: OTP Authentication filter added"); + } + if (apiKeyAuthEnabled || jwtAuthEnabled || otpAuthEnabled) { + httpSecurity + .addFilterAfter((servletRequest, servletResponse, filterChain) -> { + boolean isAuthenticated = SecurityContextHolder.getContext() != null + && SecurityContextHolder.getContext().getAuthentication() != null + && SecurityContextHolder.getContext().getAuthentication().isAuthenticated(); + log.trace("WebSecurityConfig: Redirection filters: authenticated={}", isAuthenticated); + if (isAuthenticated && (servletRequest instanceof HttpServletRequest)) { + String uri = ((HttpServletRequest)servletRequest).getRequestURI(); + log.trace("WebSecurityConfig: Redirection filters: Request uri={}", uri); + if (StringUtils.startsWithAny(uri, loginUrl, loginPage)) { + log.debug("WebSecurityConfig: Redirection filter: Redirecting {} to {}...", uri, loginSuccessUrl); + ((HttpServletResponse)servletResponse).sendRedirect(loginSuccessUrl); + } + } + filterChain.doFilter(servletRequest, servletResponse); + }, lastAuthFilterClass); + } + + if (userFormAuthEnabled) { + httpSecurity + //.authorizeHttpRequests( + // authorize -> authorize.requestMatchers("/broker/credentials", "/baguette/ref/**").hasAnyRole(ROLE_JWT_TOKEN, ROLE_API_KEY)) + .authorizeHttpRequests( + authorize -> authorize.requestMatchers("/favicon.ico", "/health").permitAll()) + .authorizeHttpRequests( + authorize -> authorize.requestMatchers(permittedUrls).permitAll()) + .authorizeHttpRequests( + authorize -> authorize.anyRequest().authenticated()); + } else { + httpSecurity + .authorizeHttpRequests( + authorize -> authorize.anyRequest().authenticated()); + } + + return httpSecurity.build(); + } + + private void checkSettings() { + // Check User Form authentication settings + boolean userFormAuthEnabled = this.userFormAuthEnabled && StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password); + if (this.userFormAuthEnabled && !userFormAuthEnabled) { + if (StringUtils.isBlank(username)) + log.warn("WebSecurityConfig: User Form authentication is enabled but -no- Username has been provided. It will not be possible to login from User form"); + if (StringUtils.isBlank(password)) + log.warn("WebSecurityConfig: User Form authentication is enabled but -no- Password has been provided. It will not be possible to login from User form"); + } + + // Check JWT Token authentication settings + // Nothing to do + + // Check API Key authentication settings + boolean apiKeyAuthEnabled = this.apiKeyAuthEnabled && StringUtils.isNotBlank(apiKeyValue) + && (StringUtils.isNotBlank(apiKeyRequestHeader) || StringUtils.isNotBlank(apiKeyRequestParam)); + if (this.apiKeyAuthEnabled && !apiKeyAuthEnabled) { + if (StringUtils.isBlank(apiKeyValue)) + log.warn("WebSecurityConfig: API Key authentication is enabled but -no- API Key has been provided. It will not be possible to authenticate using API Key"); + else + log.warn("WebSecurityConfig: API Key authentication is enabled but -no- API Key request header or parameter has been set. It will not be possible to authenticate using API Key"); + } + + // Check OTP authentication settings + boolean otpAuthEnabled = this.otpAuthEnabled + && (StringUtils.isNotBlank(otpRequestHeader) || StringUtils.isNotBlank(otpRequestParam)); + if (this.otpAuthEnabled && !otpAuthEnabled) { + log.warn("WebSecurityConfig: OTP authentication is enabled but -no- OTP request header or parameter has been set. It will not be possible to authenticate using OTP"); + } + } + + public Filter jwtAuthorizationFilter() { + return (servletRequest, servletResponse, filterChain) -> { + if (servletRequest instanceof HttpServletRequest req) { + + // Get JWT token from Authorization header + String jwtValue = req.getHeader(JwtTokenService.HEADER_STRING); + log.debug("jwtAuthorizationFilter: Authorization Header: {}", passwordUtil.encodePassword(jwtValue)); + + // ...else get JWT token from 'jwtRequestParam' query parameter + if (StringUtils.isBlank(jwtValue)) { + if (StringUtils.isNotBlank(jwtRequestParam)) { + log.debug("jwtAuthorizationFilter: Authorization Header is missing. Checking for '{}' parameter", jwtRequestParam); + jwtValue = req.getParameter(jwtRequestParam); + log.debug("jwtAuthorizationFilter: '{}' parameter value: {}", jwtRequestParam, passwordUtil.encodePassword(jwtValue)); + if (StringUtils.isNotBlank(jwtValue)) + jwtValue = JwtTokenService.TOKEN_PREFIX + jwtValue; + } else { + log.debug("jwtAuthorizationFilter: JWT token not found in headers and no JWT token parameter has been set"); + } + } + + // Check JWT token validity + if (jwtValue!=null && jwtValue.startsWith(JwtTokenService.TOKEN_PREFIX)) { + try { + log.debug("jwtAuthorizationFilter: Parsing Authorization header..."); + Claims claims = jwtTokenService.parseToken(jwtValue); + String user = claims.getSubject(); + String audience = claims.getAudience(); + log.debug("jwtAuthorizationFilter: Authorization header --> user: {}", user); + log.debug("jwtAuthorizationFilter: Authorization header --> audience: {}", audience); + if (user!=null && audience!=null) { + if (JwtTokenService.AUDIENCE_UPPERWARE.equals(audience)) { + log.debug("jwtAuthorizationFilter: JWT token is valid"); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(user, null, + Collections.singletonList(new SimpleGrantedAuthority(ROLE_JWT_TOKEN))); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("jwtAuthorizationFilter: Security context updated"); + } else { + log.debug("jwtAuthorizationFilter: Audience claim is invalid: {}", audience); + } + } else { + log.debug("jwtAuthorizationFilter: JWT token does not contain claim Audience"); + } + } catch (Exception ex) { + log.debug("jwtAuthorizationFilter: JWT token is not valid: EXCEPTION: ", ex); + } + } else { + log.debug("jwtAuthorizationFilter: No or invalid Authorization header"); + } + } else { + log.warn("jwtAuthorizationFilter: Not an HttpServletRequest"); + } + + // continue filter chain processing + filterChain.doFilter(servletRequest, servletResponse); + }; + } + + public Filter apiKeyAuthenticationFilter() { + return (servletRequest, servletResponse, filterChain) -> { + log.trace("apiKeyAuthenticationFilter: BEGIN: request={}", servletRequest); + if (StringUtils.isNotBlank(apiKeyValue)) { + if (servletRequest instanceof HttpServletRequest request && servletResponse instanceof HttpServletResponse) { + + log.trace("apiKeyAuthenticationFilter: http-request={}", request); + String apiKey = request.getHeader(apiKeyRequestHeader); + log.debug("apiKeyAuthenticationFilter: Request Header API Key: {}={}", apiKeyRequestHeader, passwordUtil.encodePassword(apiKey)); + if (StringUtils.isBlank(apiKey)) { + apiKey = request.getParameter(apiKeyRequestParam); + log.debug("apiKeyAuthenticationFilter: Request Parameter API Key: {}={}", apiKeyRequestParam, passwordUtil.encodePassword(apiKey)); + } + if (StringUtils.isNotBlank(apiKey)) { + log.debug("apiKeyAuthenticationFilter: API Key found"); + + if (apiKeyValue.equals(apiKey)) { + log.debug("apiKeyAuthenticationFilter: API Key is correct"); + try { + // construct one of Spring's auth tokens + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(apiKeyRequestHeader, apiKeyValue, + Collections.singletonList(new SimpleGrantedAuthority(ROLE_API_KEY))); + // store completed authentication in security context + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("apiKeyAuthenticationFilter: Security context has been updated"); + } catch (Exception e) { + log.error("apiKeyAuthenticationFilter: EXCEPTION: ", e); + } + } else { + log.debug("apiKeyAuthenticationFilter: API Key is incorrect"); + } + } else { + log.debug("apiKeyAuthenticationFilter: No API Key found in request headers or parameters"); + } + } else { + throw new IllegalArgumentException("API Key Authentication filter does not support non-HTTP requests and responses. Req-class: " + +servletRequest.getClass().getName()+" Resp-class: "+servletResponse.getClass().getName()); + } + } else { + log.warn("apiKeyAuthenticationFilter: No API-Key specified"); + } + + // continue down the chain + filterChain.doFilter(servletRequest, servletResponse); + }; + } + + public Filter otpAuthenticationFilter() { + return (servletRequest, servletResponse, filterChain) -> { + log.trace("OTPAuthenticationFilter: BEGIN: request={}", servletRequest); + if (otpAuthEnabled) { + if (servletRequest instanceof HttpServletRequest request && servletResponse instanceof HttpServletResponse) { + + log.trace("OTPAuthenticationFilter: http-request={}", request); + String otp = request.getHeader(otpRequestHeader); + log.debug("OTPAuthenticationFilter: Request Header OTP: {}={}", otpRequestHeader, passwordUtil.encodePassword(otp)); + if (StringUtils.isBlank(otp)) { + otp = request.getParameter(otpRequestParam); + log.debug("OTPAuthenticationFilter: Request Parameter OTP: {}={}", otpRequestParam, passwordUtil.encodePassword(otp)); + } + if (StringUtils.isNotBlank(otp)) { + log.debug("OTPAuthenticationFilter: OTP provided"); + + if (otpCache.containsKey(otp)) { + long issueTimestamp = otpCache.remove(otp); + boolean expired = (System.currentTimeMillis() - issueTimestamp) > otpDuration; + + if (!expired) { + log.debug("OTPAuthenticationFilter: OTP found in cache"); + try { + // construct one of Spring's auth tokens + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(otpRequestHeader, otp, + Collections.singletonList(new SimpleGrantedAuthority(ROLE_OTP))); + // store completed authentication in security context + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("OTPAuthenticationFilter: Security context has been updated"); + } catch (Exception e) { + log.error("OTPAuthenticationFilter: EXCEPTION: ", e); + } + } else { + log.debug("OTPAuthenticationFilter: OTP found in cache but has expired"); + } + } else { + log.debug("OTPAuthenticationFilter: OTP not found in cache"); + } + } else { + log.debug("OTPAuthenticationFilter: No OTP provided in request headers or parameters"); + } + } else { + throw new IllegalArgumentException("OTP Authentication filter does not support non-HTTP requests and responses. Req-class: " + +servletRequest.getClass().getName()+" Resp-class: "+servletResponse.getClass().getName()); + } + } else { + log.warn("OTPAuthenticationFilter: OTP is disabled"); + } + + // continue down the chain + filterChain.doFilter(servletRequest, servletResponse); + }; + } + + public String otpCreate() { + String newOtp = RandomStringUtils.randomAlphanumeric(32, 64); + otpCache.put(newOtp, System.currentTimeMillis()); + return newOtp; + } + + public long otpIssueTimestamp(String otp) { + return otpCache.get(otp); + } + + public long otpExpirationTimestamp(String otp) { + return otpCache.get(otp) + otpDuration; + } + + public long otpDuration(String otp) { + return otpDuration; + } + + public void otpRemove(String otp) { + otpCache.remove(otp); + } + + public void otpClearCache() { + otpCache.clear(); + } +} \ No newline at end of file diff --git a/ems-core/control-service/src/main/resources/META-INF/spring.factories b/ems-core/control-service/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d78b4f3 --- /dev/null +++ b/ems-core/control-service/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=gr.iccs.imu.ems.util.NetUtilPostProcessor \ No newline at end of file diff --git a/ems-core/control-service/src/main/resources/banner-0.txt b/ems-core/control-service/src/main/resources/banner-0.txt new file mode 100644 index 0000000..4a63096 --- /dev/null +++ b/ems-core/control-service/src/main/resources/banner-0.txt @@ -0,0 +1,16 @@ + + ______ __ __ _____ _____ _ _ + | ____| \/ |/ ____| / ____| | | | | + | |__ | \ / | (___ | | ___ _ __ | |_ _ __ ___ | | + | __| | |\/| |\___ \ | | / _ \| '_ \| __| '__/ _ \| | + | |____| | | |____) | | |___| (_) | | | | |_| | | (_) | | + |______|_| |_|_____/ \_____\___/|_| |_|\__|_| \___/|_| + + :: EMS Control :: (@project.version@) + :: Spring Boot :: ${spring-boot.formatted-version} + :: Java (TM) :: (${java.version}) + :: Build Num. :: @buildNumber@ + :: Build Date :: @timestamp@ + :: SCM Branch :: @scmBranch@ + :: Image Tag :: @docker.image.name@:@docker.image.tag@ + :: Description :: @build.description@ \ No newline at end of file diff --git a/ems-core/control-service/src/main/resources/banner-1.txt b/ems-core/control-service/src/main/resources/banner-1.txt new file mode 100644 index 0000000..e9eec3a --- /dev/null +++ b/ems-core/control-service/src/main/resources/banner-1.txt @@ -0,0 +1,9 @@ + + ________ ___ _____ _____ _ _ +| ___| \/ |/ ___| / __ \ | | | | +| |__ | . . |\ `--. | / \/ ___ _ __ | |_ _ __ ___ | | +| __|| |\/| | `--. \ | | / _ \| '_ \| __| '__/ _ \| | +| |___| | | |/\__/ / | \__/\ (_) | | | | |_| | | (_) | | +\____/\_| |_/\____/ \____/\___/|_| |_|\__|_| \___/|_| + + \ No newline at end of file diff --git a/ems-core/control-service/src/main/resources/banner.txt b/ems-core/control-service/src/main/resources/banner.txt new file mode 100644 index 0000000..d246d52 --- /dev/null +++ b/ems-core/control-service/src/main/resources/banner.txt @@ -0,0 +1,17 @@ + + +${AnsiColor.051} ███████╗███╗ ███╗███████╗ ██████╗ ██████╗ ███╗ ██╗████████╗██████╗ ██████╗ ██╗ +${AnsiColor.051} ██╔════╝████╗ ████║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║╚══██╔══╝██╔══██╗██╔═══██╗██║ +${AnsiColor.051} █████╗ ██╔████╔██║███████╗ ██║ ██║ ██║██╔██╗ ██║ ██║ ██████╔╝██║ ██║██║ +${AnsiColor.012} ██╔══╝ ██║╚██╔╝██║╚════██║ ██║ ██║ ██║██║╚██╗██║ ██║ ██╔══██╗██║ ██║██║ +${AnsiColor.012} ███████╗██║ ╚═╝ ██║███████║ ╚██████╗╚██████╔╝██║ ╚████║ ██║ ██║ ██║╚██████╔╝███████╗ +${AnsiColor.012} ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ + +${AnsiColor.046} :: EMS Control :: ${AnsiColor.87} (@project.version@) +${AnsiColor.046} :: Spring Boot :: ${AnsiColor.87} ${spring-boot.formatted-version} +${AnsiColor.046} :: Java (TM) :: ${AnsiColor.87} (${java.version}) +${AnsiColor.046} :: Build Num. :: ${AnsiColor.226}@buildNumber@ +${AnsiColor.046} :: Build Date :: ${AnsiColor.226}@timestamp@ +${AnsiColor.046} :: SCM Branch :: ${AnsiColor.226}@git.branch@ +${AnsiColor.046} :: Image Tag :: ${AnsiColor.226}@docker.image.name@:@docker.image.tag@ +${AnsiColor.046} :: Description :: ${AnsiColor.226}@build.description@ ${AnsiColor.DEFAULT}${AnsiStyle.NORMAL} \ No newline at end of file diff --git a/ems-core/control-service/src/main/resources/public/client.bat b/ems-core/control-service/src/main/resources/public/client.bat new file mode 100644 index 0000000..838a094 --- /dev/null +++ b/ems-core/control-service/src/main/resources/public/client.bat @@ -0,0 +1,22 @@ +@echo off +:: +:: Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +:: +:: This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +:: Esper library is used, in which case it is subject to the terms of General Public License v2.0. +:: If a copy of the MPL was not distributed with this file, you can obtain one at +:: https://www.mozilla.org/en-US/MPL/2.0/ +:: + +rem set EMS_CONFIG_DIR=. + +setlocal +rem set JAVA_OPTS= -Djavax.net.ssl.trustStore=..\config-files\broker-truststore.p12 ^ +rem -Djavax.net.ssl.trustStorePassword=melodic ^ +rem -Djavax.net.ssl.trustStoreType=pkcs12 +rem -Djavax.net.debug=all +rem -Djavax.net.debug=ssl,handshake,record + +java %JAVA_OPTS% -jar broker-client.jar %* + +endlocal diff --git a/ems-core/control-service/src/main/resources/public/client.sh b/ems-core/control-service/src/main/resources/public/client.sh new file mode 100644 index 0000000..d2ddc8b --- /dev/null +++ b/ems-core/control-service/src/main/resources/public/client.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +#EMS_CONFIG_DIR=. + +#JAVA_OPTS=-Djavax.net.ssl.trustStore=./broker-truststore.p12\ -Djavax.net.ssl.trustStorePassword=melodic\ -Djavax.net.ssl.trustStoreType=pkcs12 +# -Djavax.net.debug=all +# -Djavax.net.debug=ssl,handshake,record + +java $JAVA_OPTS -jar broker-client.jar $* diff --git a/ems-core/control-service/src/main/resources/public/favicon.ico b/ems-core/control-service/src/main/resources/public/favicon.ico new file mode 100644 index 0000000..2c5b645 Binary files /dev/null and b/ems-core/control-service/src/main/resources/public/favicon.ico differ diff --git a/ems-core/control-service/src/main/resources/public/index.html b/ems-core/control-service/src/main/resources/public/index.html new file mode 100644 index 0000000..8e74e5f --- /dev/null +++ b/ems-core/control-service/src/main/resources/public/index.html @@ -0,0 +1,442 @@ + + + + + + +

EMS - Event Generation and Publish

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Settings

+ + + + + + +
Base URL:
+ +

Send Commands to Clients

+

[List] + [Map]

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClientCommandActionsResult
+ +
+ +
+ +
+ +
+ +
+ +

Event Publishing

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SesnorClientValueActionsResult
+ +
+ +
+ +
+ +
+ +
+ +

Event Generation

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SensorClientIntervalLower ValueUpper ValueActionsResult
+ + +
+ + +
+ + +
+ + +
+ + +
+ +

Live Metrics:

+ + + +
+ + +

Statistics:

+ +

Downloads:

+ +
+ + + \ No newline at end of file diff --git a/ems-core/control-service/src/main/resources/version.txt b/ems-core/control-service/src/main/resources/version.txt new file mode 100644 index 0000000..a9bdc38 --- /dev/null +++ b/ems-core/control-service/src/main/resources/version.txt @@ -0,0 +1,8 @@ +java.version=@java.version@ +maven.version=@maven.version@ +project.name=@project.parent.name@ +project.version=@project.version@ +project.build.sourceEncoding=@project.build.sourceEncoding@ +buildNumber=@buildNumber@ +maven.build.timestamp=@maven.build.timestamp@ +timestamp=@timestamp@ \ No newline at end of file diff --git a/ems-core/pom.xml b/ems-core/pom.xml new file mode 100644 index 0000000..147ad43 --- /dev/null +++ b/ems-core/pom.xml @@ -0,0 +1,295 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.2 + + + + Event Management System + + gr.iccs.imu.ems + ems-core + ${revision} + pom + + + 7.0.0-SNAPSHOT + + + UTF-8 + + + 17 + 17 + + + 2.4 + 3.11.0 + 2.9.1 + 2.5.3 + + + 2.10.1 + + 3.13.0 + + 1.10.0 + + 7.1.0 + + 4.2.0 + + 1.18.24 + + 5.17.5 + + 3.0.5 + + 8.0.0.Final + + 2.10.0 + + 1.76 + + 32.1.2-jre + + + 2.15.2 + 2.0 + + + + web-admin + util + broker-client + broker-cep + translator + common + baguette-client + baguette-server + baguette-client-install + control-service + + + + + + org.projectlombok + lombok + ${lombok.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + org.apache.commons + commons-text + ${commons-text.version} + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.bouncycastle + bcpg-jdk18on + ${bouncy-castle.version} + + + org.bouncycastle + bcpkix-jdk18on + ${bouncy-castle.version} + + + org.bouncycastle + bcprov-jdk18on + ${bouncy-castle.version} + + + + + + + org.springframework + spring-context-indexer + true + + + + + + + maven-clean-plugin + 3.3.1 + + + + public_resources + + **/* + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.5.0 + + + + + + flatten + process-resources + + flatten + + + + + flatten-clean + clean + + clean + + + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${source-plugin.version} + true + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler.version} + + + + -parameters + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${javadoc-plugin.version} + + + aggregate + + aggregate + + package + + -Xdoclint:none + + + + attach-javadocs + + jar + + + -Xdoclint:none + + + + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + 3.2.0 + + + buildnumber-create + validate + + create + + + + buildnumber-create-metadata + validate + + create-metadata + + + + + ${project.build.directory} + + yyyy-MM-dd HH:mm:ss.SSSZ + ${project.version} + + + buildNumber + + false + false + + + + + + + + scm:git:http://127.0.0.1/dummy + scm:git:https://127.0.0.1/dummy + HEAD + http://127.0.0.1/dummy + + + diff --git a/ems-core/translator/pom.xml b/ems-core/translator/pom.xml new file mode 100644 index 0000000..41ebbcf --- /dev/null +++ b/ems-core/translator/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + translator + EMS - Translator + + + + 1.5.2 + 0.18.1 + + + + + + gr.iccs.imu.ems + broker-cep + ${project.version} + provided + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework + spring-web + + + + + org.projectlombok + lombok + provided + + + + + org.jgrapht + jgrapht-core + ${jgrapht.version} + + + org.jgrapht + jgrapht-io + ${jgrapht.version} + + + + + guru.nidi + graphviz-java-all-j2v8 + ${graphviz-java.version} + + + + diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/Grouping.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/Grouping.java new file mode 100644 index 0000000..cf2646e --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/Grouping.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate; + +public enum Grouping { + UNSPECIFIED(-1, false, false), + PER_INSTANCE(0, true, false), + PER_HOST(1, true, false), + PER_ZONE(2, false, true), + PER_REGION(3, false, true), + PER_CLOUD(4, false, true), + GLOBAL(5, false, false); + + private int order; + private boolean sameHost; + private boolean sameCloud; + + Grouping(int n, boolean sh, boolean sc) { + order = n; + sameHost = sh; + sameCloud = sc; + } + + public boolean equals(Grouping g) { + return this.order == g.order; + } + + public boolean lowerThan(Grouping g) { + return this.order < g.order; + } + + public boolean greaterThan(Grouping g) { + return this.order > g.order; + } +} \ No newline at end of file diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/NoopTranslator.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/NoopTranslator.java new file mode 100644 index 0000000..16764ee --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/NoopTranslator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@Order(Integer.MIN_VALUE) +public class NoopTranslator implements Translator { + public TranslationContext translate(String modelPath) { + log.warn("NoopTranslator: Call to 'translate': model-path={}", modelPath); + return new TranslationContext(modelPath); + } +} \ No newline at end of file diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContext.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContext.java new file mode 100644 index 0000000..a6600ee --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContext.java @@ -0,0 +1,707 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.gson.Gson; +import gr.iccs.imu.ems.translate.dag.DAG; +import gr.iccs.imu.ems.translate.dag.DAGNode; +import gr.iccs.imu.ems.translate.model.*; +import gr.iccs.imu.ems.util.FunctionDefinition; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Slf4j +@ToString +public class TranslationContext implements Serializable { + + @Getter + private final String modelName; + + // Decomposition DAG + @Getter + @JsonIgnore + private transient gr.iccs.imu.ems.translate.dag.DAG DAG; + + // Event-to-Action map + @Getter + private final Map> E2A = new HashMap<>(); + + // SLO set + @Getter + private final Set SLO = new LinkedHashSet<>(); + + // Component-to-Sensor map + @Getter + @JsonIgnore + private final transient Map> C2S = new HashMap<>(); //XXX:TODO-LOW: Convert to strings + + // Data-to-Sensor map + @Getter + @JsonIgnore + private final transient Map> D2S = new HashMap<>(); //XXX:TODO-LOW: Convert to strings + + // Sensor Monitors set + @Getter + private final Set MON = new LinkedHashSet<>(); //XXX:TODO-LOW: Remove ?? + @Getter + private final Set MONS = new LinkedHashSet<>(); + + // Grouping-to-EPL Rule map + private final Map>> G2R = new HashMap<>(); + + // Grouping-to-Topics map + private final Map> G2T = new HashMap<>(); + + // Metric-to-Metric Context map + @Getter + @JsonIgnore + private final transient Map> M2MC = new HashMap<>(); + + // Composite Metric Variables set + @Getter + @JsonIgnore + private final transient Set CMVar = new LinkedHashSet<>(); + @Getter + @JsonIgnore + private final transient Set CMVar_1 = new LinkedHashSet<>(); + + // Metric Variable Values set (i.e. non-composite metric variable) + private final Set MVV = new LinkedHashSet<>(); + private final Map MvvCP = new HashMap<>(); + + // Function set + @Getter + private final Set FUNC = new LinkedHashSet<>(); + + // Topics-Connections-per-Grouping + @JsonIgnore + private final transient Map providedTopics = new HashMap<>(); // topic-grouping where this topic is provided + @JsonIgnore + private final transient Map> requiredTopics = new HashMap<>(); // topic-set of groupings where this topic is required + protected final Map>> topicConnections = new HashMap<>(); // grouping-provided topic in grouping-groupings that require provided topic + protected boolean needsRefresh; + + // Metric Constraints + private final Set metricConstraints = new LinkedHashSet<>(); + // Logical Constraints + private final Set logicalConstraints = new LinkedHashSet<>(); + // If-Then-Else Constraints + private final Set ifThenConstraints = new LinkedHashSet<>(); + + // Load-annotated Metric + protected final Set loadAnnotatedMetricsSet = new LinkedHashSet<>(); + + // Export files + @Getter @Setter + private List exportFiles = new ArrayList<>(); + + // Element-to-Full-Name cache, pattern and count + @JsonIgnore + protected transient final Map E2N; //XXX:TODO-LOW: Clear after translation + @JsonIgnore + protected transient final AtomicLong elementsCount; + @Getter @Setter + protected String fullNamePattern; // all options: {TYPE}, {CAMEL}, {MODEL}, {ELEM}, {HASH}, {COUNT} + + @Getter + protected final Map additionalResults = new LinkedHashMap<>(); + + @JsonIgnore + private final transient Gson gson = new Gson(); // Used when cloning + /*@JsonIgnore // Alternative: clone with Jackson instead of Gson + private final transient ObjectMapper objectMapper = new ObjectMapper();*/ + + // ==================================================================================================================================================== + // Constructors + + public TranslationContext(String modelName) { + this(true, modelName); + } + + public TranslationContext(boolean initializeDag, String modelName) { + // Initialize fields + this.modelName = modelName; + this.DAG = initializeDag ? new DAG(this::getFullName) : new DAG(); + + // Element-to-Full-Name staff + this.E2N = new HashMap<>(); + this.elementsCount = new AtomicLong(0); + this.fullNamePattern = "{ELEM}"; + } + + /*public TranslationContext(TranslationContext _TC, boolean initializeDag) { + this(initializeDag, _TC.modelName); + + // Comment out 'this(...)' constructor and uncomment the following lines + //this.DAG = deepCopy( _TC.DAG, DAG.class ); // DAG used during translation. Not for serialization + //this.E2N = new HashMap<>(); + //this.elementsCount = new AtomicLong(0); + //this.fullNamePattern = "{ELEM}"; + // + //this.M2MC.putAll( cloneMapSet(_TC.M2MC) ); // Temporary translation cache. Not for serialization + + this.E2A.putAll( cloneMapSet(_TC.E2A) ); + this.SLO.addAll(_TC.SLO); + //this.C2S.putAll( cloneMapSet(_TC.C2S) ); + //this.D2S.putAll( cloneMapSet(_TC.D2S) ); + this.MON.addAll( cloneSet(_TC.MON) ); + this.MONS.addAll(_TC.MONS); + this.G2R.putAll( cloneMapMapSet(_TC.G2R) ); + this.G2T.putAll( cloneMapSet(_TC.G2T) ); + this.CMVar.addAll(_TC.CMVar); + this.CMVar_1.addAll( cloneSet(_TC.CMVar_1) ); + this.MVV.addAll(_TC.MVV); + this.MvvCP.putAll(_TC.MvvCP); + this.FUNC.addAll( cloneSet(_TC.FUNC) ); + this.providedTopics.putAll(_TC.providedTopics); + this.requiredTopics.putAll( cloneMapSet(_TC.requiredTopics) ); + this.topicConnections.putAll( cloneMapMapSet(_TC.topicConnections) ); + this.needsRefresh = _TC.needsRefresh; + this.metricConstraints.addAll( cloneSet(_TC.metricConstraints) ); + this.logicalConstraints.addAll( cloneSet(_TC.logicalConstraints) ); + this.ifThenConstraints.addAll( cloneSet(_TC.ifThenConstraints) ); + this.loadAnnotatedMetricsSet.addAll(_TC.loadAnnotatedMetricsSet); + this.exportFiles.addAll(_TC.exportFiles); + this.fullNamePattern = cloneObject(_TC.fullNamePattern); + } + + // ==================================================================================================================================================== + // Cloning methods + + public TranslationContext clone() { + return new TranslationContext(this, false); + } + + @SneakyThrows + protected T deepCopy(T object, Class type) { + return gson.fromJson(gson.toJson(object, type), type); + *//*return objectMapper.readValue( + objectMapper.writeValueAsString(object), type);*//* + } + + protected T cloneObject(T obj) { + if (obj==null) return null; + if (obj instanceof String x) return (T) new String(x); + + if (obj instanceof PullSensor x) return (T) deepCopy(x, PullSensor.class); + if (obj instanceof PushSensor x) return (T) deepCopy(x, PushSensor.class); + if (obj instanceof Sensor x) return (T) deepCopy(x, Sensor.class); + if (obj instanceof Component x) return (T) deepCopy(x, Component.class); + if (obj instanceof Data x) return (T) deepCopy(x, Data.class); + if (obj instanceof Monitor x) return (T) deepCopy(x, Monitor.class); + + if (obj instanceof MetricVariable x) return (T) deepCopy(x, MetricVariable.class); + if (obj instanceof Metric x) return (T) deepCopy(x, Metric.class); + if (obj instanceof MetricContext x) return (T) deepCopy(x, MetricContext.class); + if (obj instanceof FunctionDefinition x) return (T) deepCopy(x, FunctionDefinition.class); + + if (obj instanceof MetricConstraint x) return (T) deepCopy(x, MetricConstraint.class); + if (obj instanceof LogicalConstraint x) return (T) deepCopy(x, LogicalConstraint.class); + if (obj instanceof IfThenConstraint x) return (T) deepCopy(x, IfThenConstraint.class); + if (obj instanceof Constraint x) return (T) deepCopy(x, Constraint.class); + + throw new IllegalArgumentException("Unsupported type: "+obj.getClass().getName()); + } + + protected Set cloneSet(Set set) { + return set.stream() + .map(this::cloneObject) + .collect(Collectors.toSet()); + } + + protected Map> cloneMapSet(Map> map) { + return map.entrySet().stream() + .collect(Collectors.toMap( + e -> cloneObject(e.getKey()), + e -> cloneSet(e.getValue()) + )); + } + + protected Map>> cloneMapMapSet(Map>> map) { + return map.entrySet().stream() + .collect(Collectors.toMap( + e -> cloneObject(e.getKey()), + e -> cloneMapSet(e.getValue()) + )); + }*/ + + // ==================================================================================================================================================== + // Copy/Getter methods + + public Map> getG2T() { + if (G2T==null) return Collections.emptyMap(); + HashMap> newMap = new HashMap<>(); + G2T.forEach((key, value) -> newMap.put(key, new HashSet<>(value))); + return newMap; + } + + public Map>> getG2R() { + if (G2R==null) return Collections.emptyMap(); + Map>> newGroupingsMap = new HashMap<>(); // groupings + G2R.forEach((key, value) -> { + Map> newTopicsMap = new HashMap<>(); // topics per grouping + newGroupingsMap.put(key, newTopicsMap); + value.forEach((key1, value1) -> { + Set newRuleSet = new HashSet<>(); // rules per topic per grouping + newTopicsMap.put(key1, newRuleSet); + newRuleSet.addAll(value1); + }); + }); + return newGroupingsMap; + } + + public MetricContext getMetricContextForMetric(Metric m) { + if (M2MC==null) return null; + Set set = M2MC.get(m); + return set == null ? null : set.iterator().next(); + } + + public Set getMetricConstraints() { + return metricConstraints!=null ? new HashSet<>(metricConstraints) : Collections.emptySet(); + } + + public Set getLogicalConstraints() { + return logicalConstraints!=null ? new HashSet<>(logicalConstraints) : Collections.emptySet(); + } + + public boolean isMVV(String name) { + if (MVV==null) + return false;; + for (String mvv : MVV) + if (mvv.equals(name)) return true; + return false; + } + + public Set getMVV() { + return MVV!=null ? new HashSet<>(MVV) : Collections.emptySet(); + } + + public Map getCompositeMetricVariables() { + return MvvCP!=null ? new HashMap<>(MvvCP) : Collections.emptyMap(); + } + + // ==================================================================================================================================================== + // Map- and Set-related helper methods + + @SuppressWarnings("unchecked") + protected void _addPair(Map map, Object key, Object value) { + Set valueSet = (Set) map.get(key); + if (valueSet == null) { + valueSet = new HashSet<>(); + map.put(key, valueSet); + } + if (value instanceof List) valueSet.addAll((List) value); + else valueSet.add(value); + } + + public void addEventActionPair(Event event, Action action) { + _addPair(E2A, E2N.get(event), E2N.get(action)); + } + + public void addEventActionPairs(Event event, List actions) { + _addPair(E2A, E2N.get(event), actions.stream().map(E2N::get).collect(Collectors.toList())); + } + + public void addSLO(ServiceLevelObjective slo) { + if (E2N.get(slo)!=null) SLO.add(E2N.get(slo)); + else SLO.add(slo.getName()); + } + + public void addComponentSensorPair(ObjectContext objContext, Sensor sensor) { + if (objContext != null) { + Component comp = objContext.getComponent(); + Data data = objContext.getData(); + if (comp != null) _addPair(C2S, comp, sensor); + if (data != null) _addPair(D2S, data, sensor); + } else { + _addPair(C2S, null, sensor); + } + } + + public void addMonitorsForSensor(String sensorName, Set monitors) { + if (monitors != null) { + if (!MONS.contains(sensorName)) { + MON.addAll(monitors); + MONS.add(sensorName); + } + } + } + + public boolean containsMonitorsForSensor(String sensorName) { + return MONS.contains(sensorName); + } + + public Set getMonitors() { + return Collections.unmodifiableSet(MON); + } + + public void addGroupingTopicPair(String grouping, String topic) { + _addPair(G2T, grouping, topic); + } + + public void addGroupingTopicPairs(String grouping, List topics) { + _addPair(G2T, grouping, topics); + } + + public void addGroupingRulePair(String grouping, String topic, String rule) { + Map> topics = G2R.computeIfAbsent(grouping, k -> new HashMap<>()); + Set rules = topics.computeIfAbsent(topic, k -> new HashSet<>()); + rules.add(rule); + } + + public void addGroupingRulePairs(String grouping, String topic, List rules) { + rules.forEach(rule -> addGroupingRulePair(grouping, topic, rule)); + } + + public void addMetricMetricContextPair(Metric m, MetricContext mc) { + _addPair(M2MC, m, mc); + } + + public void addMetricMetricContextPairs(Metric m, List mcs) { + _addPair(M2MC, m, mcs); + } + + public void addCompositeMetricVariable(MetricVariable mv) { + CMVar.add(mv.getName()); + CMVar_1.add(mv); + } + + public void addCompositeMetricVariables(List mvs) { + mvs.forEach(this::addCompositeMetricVariable); + } + + public void addMVV(@NonNull String mvv) { + MVV.add(mvv); + } + + public void addMVV(MetricVariable mvv) { + MVV.add(mvv.getName()); + } + + public void addMVVs(List mvvs) { + mvvs.forEach(this::addMVV); + } + + public void addFunction(Function f) { + FunctionDefinition fdef = new FunctionDefinition().setName(f.getName()).setExpression(f.getExpression()).setArguments(f.getArguments()); + FUNC.add(fdef); + } + + public void addMetricConstraint(UnaryConstraint uc) { + // Get comparison operator + ComparisonOperatorType op = uc.getComparisonOperator(); + if (op==null) + throw new IllegalArgumentException("Metric Constraint '"+uc.getName()+"' has no operator specified"); + + // Get metric context/variable name + String metricName = null; + if (uc instanceof MetricConstraint mc) { + MetricContext context = mc.getMetricContext(); + if (context!=null) metricName = context.getName(); + if (StringUtils.isBlank(metricName)) + throw new IllegalArgumentException("Metric Constraint '"+mc.getName()+"' has no valid metric context"); + } else + if (uc instanceof MetricVariableConstraint mvc) { + MetricVariable mv = mvc.getMetricVariable(); + if (mv!=null) metricName = mv.getName(); + if (StringUtils.isBlank(metricName)) + throw new IllegalArgumentException("Metric Variable Constraint '"+uc.getName()+"' has no valid metric variable"); + } else + throw new IllegalArgumentException("Invalid Unary Constraint '"+uc.getName()+"' specified. Only metric constraints and metric variable constraints are allowed."); + + // Add threshold information + metricConstraints.add( + MetricConstraint.builder() + .name(uc.getName()) + .comparisonOperator(op) + .threshold(uc.getThreshold()) + .build() + ); + } + + public void addLogicalConstraint(LogicalConstraint logicalConstraint, List nodeList) { + String name = logicalConstraint.getName(); + + // Check there is a logical operator + LogicalOperatorType op = logicalConstraint.getLogicalOperator(); + if (op==null) + throw new IllegalArgumentException("Logical Constraint '"+name+"' has no operator specified"); + + // Check there are child constraints + List childConstraintNames = logicalConstraint.getConstraints() + .stream().map(NamedElement::getName).toList(); + if (childConstraintNames.size()==0) + throw new IllegalArgumentException("Logical Constraint '"+name+"' has no child constraints"); + + // Add logical constraint information + logicalConstraints.add(logicalConstraint); + } + + public void addIfThenConstraint(@NonNull IfThenConstraint ifThenConstraint) { + String name = ifThenConstraint.getName(); + + // Check child constraints + Constraint ifConstraint = ifThenConstraint.getIf(); + Constraint thenConstraint = ifThenConstraint.getThen(); + Constraint elseConstraint = ifThenConstraint.getElse(); + if (ifConstraint==null || thenConstraint==null) + throw new IllegalArgumentException("If-Then-Else Constraint '"+name+"' has no IF or no THEN constraint"); + String ifConstraintName = ifConstraint.getName(); + String thenConstraintName = thenConstraint.getName(); + if (StringUtils.isBlank(ifConstraintName) || StringUtils.isBlank(thenConstraintName)) + throw new IllegalArgumentException("IF or THEN constraint in If-Then-Else constraint'"+name+"' has no name"); + String elseConstraintName = elseConstraint != null ? elseConstraint.getName() : null; + if (elseConstraint!=null && StringUtils.isBlank(elseConstraintName)) + throw new IllegalArgumentException("ELSE constraint in If-Then-Else constraint'"+name+"' has no name"); + + // Add if-then-else constraint information + ifThenConstraints.add(ifThenConstraint); + } + + // ==================================================================================================================================================== + // Topic-Connections-per-Grouping-related helper methods + // Auto-fill of Topic connections between Groupings.... (use provide/require methods below) + + public void provideGroupingTopicPair(String grouping, String topic) { + if (isMVV(topic)) return; + addGroupingTopicPair(grouping, topic); + String providerGrouping = providedTopics.get(grouping); + if (providerGrouping != null && !providerGrouping.equals(grouping)) { + throw new IllegalArgumentException("Topic " + topic + " is provided more than once: grouping-1=" + grouping + ", grouping-2=" + providedTopics.get(grouping)); + } + providedTopics.put(topic, grouping); + needsRefresh = true; + } + + public void requireGroupingTopicPair(String grouping, String topic) { + log.debug("requireGroupingTopicPair: grouping={}, topic={}", grouping, topic); + if (isMVV(topic)) return; + log.trace("requireGroupingTopicPair: Not an MVV. Good: grouping={}, topic={}", grouping, topic); + log.trace("requireGroupingTopicPair: requiredTopics BEFORE: {}", requiredTopics); + addGroupingTopicPair(grouping, topic); + Set groupings = requiredTopics.computeIfAbsent(topic, k -> new HashSet<>()); + groupings.add(grouping); + needsRefresh = true; + log.trace("requireGroupingTopicPair: requiredTopics AFTER: {}", requiredTopics); + } + + public void requireGroupingTopicPairs(String grouping, List topics) { + topics.forEach(t -> requireGroupingTopicPair(grouping, t)); + } + + public Map>> getTopicConnections() { + if (needsRefresh) { + log.debug("TranslationContext.getTopicConnections(): Topic connections need refresh"); + topicConnections.clear(); + + log.debug("TranslationContext.getTopicConnections(): required-topics={}, provided-topics={}", requiredTopics, providedTopics); + + // for every required topic... + for (Map.Entry> pair : requiredTopics.entrySet()) { + // get consumer topics for current required topic + String requiredTopic = pair.getKey(); + Set consumerGroupings = pair.getValue(); + // get provider grouping of current required topic + String providerGrouping = providedTopics.get(requiredTopic); + if (providerGrouping == null) + throw new IllegalArgumentException("Topic " + requiredTopic + " is not provided in any grouping"); + // remove provider grouping from consumer groupings + consumerGroupings.remove(providerGrouping); + // store required topic in 'topicConnections' + if (consumerGroupings.size() > 0) { + // ...get provider grouping topics from topicConnections + Map> groupingTopics = topicConnections.computeIfAbsent(providerGrouping, k -> new HashMap<>()); + // ...store consumer groupings for current required topic in provider grouping + if (groupingTopics.containsKey(requiredTopic)) + throw new IllegalArgumentException("INTERNAL ERROR: Required Topic " + requiredTopic + " is already set in provider grouping " + providerGrouping + " in '_TC.topicConnections'"); + groupingTopics.put(requiredTopic, consumerGroupings); + } + } + + needsRefresh = false; + log.debug("TranslationContext.getTopicConnections(): Topic connections refreshed: {}", topicConnections); + } else { + log.debug("TranslationContext.getTopicConnections(): No need to refresh Topic connections. Returning from cache: {}", topicConnections); + } + return topicConnections; + } + + public Map> getTopicConnectionsForGrouping(String grouping) { + return getTopicConnections().get(grouping); + } + + // ==================================================================================================================================================== + // Element full name generation methods + + public String getFullName(NamedElement elem) { + log.trace(" getFullName: BEGIN: {}", elem); + if (elem == null) return null; + log.trace(" getFullName: NULL check OK: name={}", elem.getName()); + + // return cached full-name for element + String fullName = E2N.get(elem); + log.trace(" getFullName: Cached Name: {}", fullName); + if (fullName != null) return fullName; + log.trace(" getFullName: NO Cached Name:..."); + + // else generate full-name for element (and cache it) + String elemName = elem.getName(); + log.trace(" getFullName: elem-name={}", elemName); + String elemType = _getElementType(elem); + log.trace(" getFullName: elem-type={}", elemType); + log.trace(" getFullName: elem-eContainer={}", elem.getContainer()); + String modelName = elem.getContainer()!=null + ? elem.getContainer().getName() : null; + log.trace(" getFullName: model-name={}", modelName); + log.trace(" getFullName: elem-eContainer-eContainer={}", + elem.getContainer()!=null ? elem.getContainer().getContainer() : null); + String camelName = elem.getContainer()!=null && elem.getContainer().getContainer()!=null + ? elem.getContainer().getContainer().getName() : null; + log.trace(" getFullName: camel-name={}", camelName); + + fullName = fullNamePattern + .replace("{TYPE}", elemType) + .replace("{CAMEL}", Objects.requireNonNullElse(camelName, "C")) + .replace("{MODEL}", Objects.requireNonNullElse(modelName, "M")) + .replace("{ELEM}", elemName) + .replace("{HASH}", Integer.toString(elemName.hashCode())) + .replace("{COUNT}", Long.toString(elementsCount.getAndIncrement())) + ; + log.trace(" getFullName: New Full name={}", fullName); + + E2N.put(elem, fullName); + log.trace(" getFullName: END: Cached new FULL name: {}", fullName); + + return fullName; + } + + public void addElementToNamePair(@NonNull NamedElement elem, @NonNull String fullName) { + E2N.put(elem, fullName); + } + + protected String _getElementType(NamedElement e) { + if (e==null) { + log.error("Null element passed"); + } + else if (e instanceof ScalabilityRule) return "RUL"; + else if (e instanceof Event) return "EVT"; + else if (e instanceof Constraint) return "CON"; + else if (e instanceof MetricVariable) return "VAR"; + else if (e instanceof MetricContext) return "CTX"; + else if (e instanceof Metric) return "MET"; + else if (e instanceof MetricTemplate) return "TMP"; + else if (e instanceof OptimisationRequirement) return "OPT"; + else if (e instanceof ServiceLevelObjective) return "SLO"; + else if (e instanceof Requirement) return "REQ"; + else if (e instanceof ObjectContext) return "OBJ"; + else if (e instanceof Sensor) return "SNR"; + else if (e instanceof Function) return "FUN"; //XXX:TODO: Or FunctionDefinition ?? + else if (e instanceof Schedule) return "CTX"; + else if (e instanceof Window) return "CTX"; + else if (e instanceof ScalingAction) return "ACT"; + else { + //throw new ModelAnalysisException( String.format("Unknown element type: %s class=%s", e.getName(), e.getClass().getName()) ); + log.error("Unknown element type: {} class={}", e.getName(), e.getClass().getName()); + } + return "XXX"; + } + + // ==================================================================================================================================================== + // Function-Definition-related helper methods + + public Set getFunctionDefinitions() { + return new HashSet<>(FUNC); + } + + // ==================================================================================================================================================== + // Load-Metrics-related helper methods + + public void addLoadAnnotatedMetric(@NonNull String metricName) { + loadAnnotatedMetricsSet.add(metricName); + } + + public void addLoadAnnotatedMetrics(@NonNull Set metricNames) { + loadAnnotatedMetricsSet.addAll(metricNames); + } + + public Set getLoadAnnotatedMetricsSet() { + return new HashSet<>(loadAnnotatedMetricsSet); + } + + // ==================================================================================================================================================== + // Additional results helper methods + + public T getAdditionalResultsAs(String key, Class clazz) { + if (getAdditionalResults()==null) return null; + Object result = getAdditionalResults().get(key); + if (result==null) return null; + return clazz.cast(result); + } + + // ==================================================================================================================================================== + + /*public void prepareForSerialization() { + setDagForSerialization(TranslationContext.convertToSerializableDag(getDAG())); + } + + public void updateAfterSerialization() { + if (DAG!=null) { + DAG.clearDAG(); + } else { + DAG = new DAG(this::getFullName); + } + convertToDAG(this.dagForSerialization, this.DAG); + } + + public static Dag convertToSerializableDag(DAG dag) { + return new Dag( + dag.getAllDAGNodes(), + dag.getAllDAGEdges().stream() + .map(edge->new Edge(edge.getId(), edge.getSource().getId(), edge.getTarget().getId())) + .collect(Collectors.toSet()) + ); + } + + public static void convertToDAG(Dag sourceDag, DAG targetDAG) { + final Map vertices = new HashMap<>(); + sourceDag.getNodes().forEach(node -> { + targetDAG.addDAGNode(node); + vertices.put(node.getId(), node); + }); + sourceDag.getEdges().forEach(edge -> { + DAGNode src = vertices.get(edge.getSourceId()); + DAGNode trg = vertices.get(edge.getTargetId()); + targetDAG.addDAGEdge(src, trg); + }); + } + + @lombok.Data + public static class Edge implements Serializable { + private final long id; + private final long sourceId; + private final long targetId; + } + + @lombok.Data + public static class Dag implements Serializable { + private final Set nodes; + private final Set edges; + }*/ +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContextPrinter.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContextPrinter.java new file mode 100644 index 0000000..51a9996 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContextPrinter.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate; + +import gr.iccs.imu.ems.translate.model.NamedElement; +import gr.iccs.imu.ems.util.FunctionDefinition; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TranslationContextPrinter { + private final TranslationContextPrinterProperties properties; + + public void printResults(TranslationContext _TC, String exportName) { + if (! properties.isPrintResults()) { + log.debug("TranslationContextPrinter.printResults(): Translation results printing is disabled"); + return; + } + + // Print analysis results + log.info("*********************************************************"); + log.info("**** T R A N S L A T I O N R E S U L T S ****"); + log.info("*********************************************************"); + log.info("Model Name: {}", _TC.getModelName()); + + // Print DAG + String dot = null; + if (properties.getDag().isExportToDotEnabled()) { + log.info("Decomposition Graph:\n{}", _TC.getDAG()); + log.info("*********************************************************"); + try { + if (_TC.getDAG().getRootNode()!=null) { + dot = _TC.getDAG().exportToDot(); + log.info("Decomposition Graph in DOT format:\n{}", dot); + } else { + log.warn("Decomposition Graph is empty."); + log.warn("Translation Context loaded from cache?"); + } + } catch (Exception ex) { + log.error("Decomposition Graph in DOT format: EXCEPTION: ", ex); + } + } + // Export DAG to files + if (properties.getDag().isExportToFileEnabled()) { + log.info("*********************************************************"); + log.info("Decomposition Graph export to file(s)"); + try { + // Get graph export configuration + String exportPath = properties.getDag().getExportPath(); + String[] exportFormats = properties.getDag().getExportFormats(); + int imageWidth = properties.getDag().getExportImageWidth(); + + // Get base name and path of export files + if (exportPath == null) exportPath = ""; + exportName = StringUtils.stripToEmpty(exportName); + if (exportName.isEmpty()) exportName = "noname"; + String baseFileName = String.format("%s/%s-%d", exportPath, exportName, System.currentTimeMillis()); + List exportFiles; + if (dot!=null) { + exportFiles = _TC.getDAG().exportDAG(dot, baseFileName, exportFormats, imageWidth); + } else { + exportFiles = _TC.getDAG().exportDAG(baseFileName, exportFormats, imageWidth); + } + _TC.setExportFiles(exportFiles); + //log.info("Decomposition Graph export to file(s): ok"); + } catch (Exception ex) { + log.error("Decomposition Graph export to file(s): EXCEPTION: ", ex); + } + } + + // Print other translation results + log.info("*********************************************************"); + log.info("Event-to-Action map:\n{}", map2string( _TC.getE2A() )); + log.info("*********************************************************"); + log.info("SLO set:\n{}", _TC.getSLO() ); + log.info("*********************************************************"); + log.info("Component-to-Sensor map:\n{}", map2string( _TC.getC2S() )); + log.info("*********************************************************"); + log.info("Data-to-Sensor map:\n{}", map2string( _TC.getD2S() )); + log.info("*********************************************************"); + log.info("Monitors:\n {}", _TC.getMONS() ); + log.info("*********************************************************"); + log.info("Grouping-to-EPL Rules map:\n{}", prettifyG2R(_TC.getG2R(), "")); + log.info("*********************************************************"); + log.info("Grouping-to-Topics map:\n{}", _TC.getG2T()); + log.info("*********************************************************"); + log.info("Topics-Connections map:\n{}", _TC.getTopicConnections()); + log.info("*********************************************************"); + log.info("Metric-to-Metric Context map:\n{}", map2string(_TC.getM2MC())); + log.info("*********************************************************"); + log.info("MVV set:\n{}", _TC.getMVV()); + log.info("*********************************************************"); + log.info("MVV_CP map:\n{}", _TC.getCompositeMetricVariables()); + log.info("*********************************************************"); + log.info("CMVAR set:\n{}", _TC.getCMVar()); + log.info("*********************************************************"); + log.info("Function Definitions set:\n{}", getFunctionNames(_TC.getFUNC())); + log.info("*********************************************************"); + log.info("Metric Constraints:\n{}", _TC.getMetricConstraints()); + log.info("*********************************************************"); + log.info("Load-Annotated Metrics:\n{}", _TC.getLoadAnnotatedMetricsSet()); + log.info("*********************************************************"); + log.info("Additional Results:\n{}", _TC.getAdditionalResults()); + log.info("*********************************************************"); + log.info("Export files:\n{}", _TC.getExportFiles()); + log.info("*********************************************************"); + } + + public String prettifyG2R(Map>> map, String startIdent) { + StringBuilder sb = new StringBuilder(); + String ident2 = startIdent+" "; + String ident3 = startIdent+" "; + String ident4 = startIdent+"\n "; + map.forEach((groupingName, groupingTopics) -> { + sb.append(startIdent).append("-----------------------\n"); + sb.append(startIdent).append(groupingName).append(": \n"); + groupingTopics.forEach((topicName, topicRules) -> { + sb.append(ident2).append(topicName).append(": \n"); + topicRules.forEach( + ruleStr -> { + ruleStr = ruleStr + .replace("\r\n", ident4) + .replace("\n", ident4); + sb.append(ident3).append("- ").append(ruleStr).append("\n"); + } + ); + }); + }); + return sb.toString(); + } + + protected Map> map2string(Map map) { + if (map==null) return null; + Map> newMap = new HashMap<>(); + for (Object key : map.keySet()) { + Set values = (Set) map.get(key); + ArrayList list = new ArrayList<>(); + if (key==null) { + newMap.put( key+"::"+key, list ); + } else + if (key instanceof NamedElement) { + newMap.put( key.getClass().getSimpleName()+"::"+((NamedElement)key).getName(), list ); + } else { + newMap.put( key.getClass().getSimpleName()+"::"+key, list ); + } + for (Object val : values) { + if (val instanceof NamedElement) { + list.add( val.getClass().getSimpleName()+"::"+((NamedElement)val).getName() ); + } else { + list.add( val.getClass().getSimpleName()+"::"+val ); + } + } + } + return newMap; + } + + protected Collection getFunctionNames(Collection col) { + return col.stream() + .map(FunctionDefinition::getName) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContextPrinterProperties.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContextPrinterProperties.java new file mode 100644 index 0000000..81af3b9 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslationContextPrinterProperties.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate; + +import gr.iccs.imu.ems.util.EmsConstant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +@Slf4j +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX + "translator") +public class TranslationContextPrinterProperties implements InitializingBean { + private boolean printResults = true; + private Dag dag = new Dag(); + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("TranslationContextPrinterProperties: {}", this); + } + + @Data + public static class Dag { + // Graph rendering/export + private boolean exportToDotEnabled = true; + private boolean exportToFileEnabled = true; + + // Graph rendering parameters + private String exportPath; + private String[] exportFormats; + private int exportImageWidth = -1; + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/Translator.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/Translator.java new file mode 100644 index 0000000..159646e --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/Translator.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate; + +public interface Translator { + TranslationContext translate(String modelPath); +} \ No newline at end of file diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslatorApplication.java_OFF b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslatorApplication.java_OFF new file mode 100644 index 0000000..9f2b62c --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/TranslatorApplication.java_OFF @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate; + +import gr.iccs.imu.ems.translate.camel.CamelToEplTranslator; +import gr.iccs.imu.ems.translate.camel.properties.CamelToEplTranslatorProperties; +import gr.iccs.imu.ems.translate.camel.properties.RuleTemplateProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/* + * Run the 'TranslatorApplication' from command line + * + * 1) Compile application and retrieve dependencies: + * mvn clean package + * mvn dependency:copy-dependencies + * + * 2) Start CDO server and set its address (+ other settings) in CDO client config: + * File: eu.paasage.mddb.cdo.client.properties + * Property: host + * + * 3) Set environment variables: + * PAASAGE_CONFIG_DIR=.... + * EMS_CONFIG_DIR=.... + * SPRING_CONFIG_LOCATION=classpath:rule-templates.yml,file:${EMS_CONFIG_DIR}/ems-server.yml + * + * 4) Run the application: + * Windows: + * java -cp target\classes;target\dependency\* gr.iccs.imu.ems.translate.TranslatorApplication ...<>... + * Linux: + * java -cp target/classes:target/dependency/* gr.iccs.imu.ems.translate.TranslatorApplication ...<>... + */ +@Slf4j +@SpringBootApplication +public class TranslatorApplication implements CommandLineRunner { + + private static boolean standalone = false; + @Autowired + private CamelToEplTranslator translator; + @Autowired + private CamelToEplTranslatorProperties properties; + @Autowired + private RuleTemplateProperties ruleTemplates; + + public static void main(String[] args) { + standalone = true; + SpringApplication.run(TranslatorApplication.class, args); + } + + @Override + public void run(String... args) { + if (!standalone) return; // Execute only if called by 'main()' + + log.info("Testing CAMEL-to-EPL Translator"); + log.info("Args: {}", java.util.Arrays.asList(args)); + log.info("Properties: {}", properties); + log.info("Rule Templates: {}", ruleTemplates); + + String camelModelPath = (args.length > 0 && !args[0].trim().isEmpty()) ? args[0].trim() : "/camel-model"; + log.info("Camel-model: {}", camelModelPath); + translator.translate(camelModelPath); + } +} \ No newline at end of file diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAG.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAG.java new file mode 100644 index 0000000..51dde05 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAG.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.dag; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import gr.iccs.imu.ems.translate.model.NamedElement; +import guru.nidi.graphviz.engine.Format; +import guru.nidi.graphviz.engine.Graphviz; +import guru.nidi.graphviz.engine.GraphvizV8Engine; +import guru.nidi.graphviz.model.MutableGraph; +import guru.nidi.graphviz.parse.Parser; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.jgrapht.graph.DirectedAcyclicGraph; +import org.jgrapht.nio.Attribute; +import org.jgrapht.nio.AttributeType; +import org.jgrapht.nio.DefaultAttribute; +import org.jgrapht.nio.dot.DOTExporter; + +import java.io.File; +import java.io.StringWriter; +import java.io.Writer; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +public class DAG { + // Graph-related fields + @JsonIgnore + private transient Function fullNameProvider = NamedElement::getName; + private DirectedAcyclicGraph _graph; + @JsonIgnore + private transient DAGNode _root; + @JsonIgnore + private transient Map _namedElementToNodesMapping; + @JsonIgnore + private transient Map _nameToNodesMapping; + + + public DAG() { + // let everything 'null' + } + + public DAG(Function fullNameProvider) { + this.fullNameProvider = fullNameProvider; + _graph = new DirectedAcyclicGraph<>(DAGEdge.class); + _root = new DAGNode(); + _graph.addVertex(_root); + _namedElementToNodesMapping = new HashMap<>(); + _nameToNodesMapping = new HashMap<>(); + } + + public DAGNode getRootNode() { + return _root; + } + + public Set getTopLevelNodes() { + log.debug("DAG.getTopLevelNodes()"); + if (_graph==null || _root==null) { + log.debug("DAG.getTopLevelNodes(): _graph or _root is null. Returning empty set"); + return Collections.emptySet(); + } + Set children = _graph.outgoingEdgesOf(_root).stream() + .map(DAGEdge::getTarget) + .collect(Collectors.toSet()); + log.debug("DAG.getTopLevelNodes(): top-level-nodes={}", children); + return children; + } + + public boolean isTopLevelNode(DAGNode node) { + Set parents = getParentNodes(node); + for (DAGNode parent : parents) { + if (parent == _root) return true; + } + return false; + } + + public Set getLeafNodes() { + Iterator it = _graph.iterator(); + Set leafs = new HashSet<>(); + it.forEachRemaining(node -> { + if (node != _root && _graph.outgoingEdgesOf(node).isEmpty()) { + leafs.add(node); + } + }); + return leafs; + } + + public Set getParentNodes(DAGNode node) { + Set edges = _graph.incomingEdgesOf(node); + return edges.stream().map(DAGEdge::getSource).collect(Collectors.toSet()); + } + + public Set getNodeChildren(DAGNode node) { + try { + //log.debug("DAG.getNodeChildren(): node={}", node); + Set children = _graph.outgoingEdgesOf(node).stream().map(DAGEdge::getTarget).collect(java.util.stream.Collectors.toSet()); + //log.debug("DAG.getNodeChildren(): parent={}, children={}", node, children); + return children; + } catch (IllegalArgumentException iae) { + log.warn("DAG.getNodeChildren(): Node not in DAG: node={}", node); + return null; + } + } + + // ==================================================================================================================================================== + // Add node methods + + public DAGNode addTopLevelNode(NamedElement elem) { + return addTopLevelNode(elem, null); + } + + public DAGNode addTopLevelNode(NamedElement elem, String effectiveFullName) { + if (elem == null) throw new IllegalArgumentException("DAG.addTopLevelNode(): Argument #1 cannot be null"); + + log.debug("DAG.addTopLevelNode(): top-level-element={}, effective-full-name={}", elem.getName(), effectiveFullName); + DAGNode node = _namedElementToNodesMapping.get(elem); + log.debug("DAG.addTopLevelNode(): cached-node={}", node); + if (node != null && effectiveFullName != null && !effectiveFullName.trim().isEmpty() && !node.getName().equals(effectiveFullName)) { + log.debug("DAG.addTopLevelNode(): Cached-node has different full-name than effective-full-name. A new node will be created: {} != {}", node.getName(), effectiveFullName); + node = null; + } + boolean newNode = false; + if (node == null) { + String fullName = (effectiveFullName == null || effectiveFullName.trim().isEmpty()) ? fullNameProvider.apply(elem) : effectiveFullName.trim(); + + if (!_nameToNodesMapping.containsKey(fullName)) { + + node = new DAGNode(elem, fullName); + newNode = _graph.addVertex(node); + if (newNode) log.debug("DAG.addTopLevelNode(): Element added in DAG: {}", node.getName()); + else log.debug("DAG.addTopLevelNode(): Element already in DAG and replaced: {}", node.getName()); + + _namedElementToNodesMapping.put(elem, node); + if (_nameToNodesMapping.put(node.getName(), node) != null) { + log.warn("DAG.addTopLevelNode(): _nameToNodesMapping: {}", _nameToNodesMapping); + throw new RuntimeException("Element name already exists in DAG: " + node.getName()); + } + + } else { + node = _nameToNodesMapping.get(fullName); + newNode = _graph.addVertex(node); + if (newNode) log.debug("DAG.addTopLevelNode()-2: Element added in DAG: {}", node.getName()); + else log.debug("DAG.addTopLevelNode()-2: Element already in DAG and replaced: {}", node.getName()); + + _namedElementToNodesMapping.put(elem, node); + } + } else { + log.debug("DAG.addTopLevelNode(): Element already in DAG: {}", node.getName()); + } + + DAGEdge edge = new DAGEdge(); + boolean newEdge = _graph.addEdge(_root, node, edge); + if (newNode) log.debug("DAG.addTopLevelNode(): Element set as Top-Level in DAG: {}", node.getName()); + else log.debug("DAG.addTopLevelNode(): Element is already set as Top-Level in DAG: {}", node.getName()); + + return node; + } + + public DAGNode addNode(NamedElement parent, NamedElement elem) { + if (parent == null) throw new IllegalArgumentException("DAG.addNode(): Argument #1 'parent' cannot be null"); + if (elem == null) throw new IllegalArgumentException("DAG.addNode(): Argument #2 'elem' cannot be null"); + + log.debug("DAG.addNode(): parent={}, element={}", parent.getName(), elem.getName()); + DAGNode node = _namedElementToNodesMapping.get(elem); + log.debug("DAG.addNode(): cached-node={}", node); + boolean newNode = false; + if (node == null) { + String fullName = fullNameProvider.apply(elem); + + if (!_nameToNodesMapping.containsKey(fullName)) { + + node = new DAGNode(elem, fullName); + newNode = _graph.addVertex(node); + if (newNode) log.debug("DAG.addNode(): Element added in DAG: {}", node.getName()); + else log.debug("DAG.addNode(): Element already in DAG and replaced: {}", node.getName()); + + _namedElementToNodesMapping.put(elem, node); + if (_nameToNodesMapping.put(node.getName(), node) != null) { + log.warn("DAG.addNode(): _nameToNodesMapping: {}", _nameToNodesMapping); + throw new RuntimeException("Element name already exists in DAG: " + node.getName()); + } + + } else { + node = _nameToNodesMapping.get(fullName); + newNode = _graph.addVertex(node); + if (newNode) log.debug("DAG.addNode()-2: Element added in DAG: {}", node.getName()); + else log.debug("DAG.addNode()-2: Element already in DAG and replaced: {}", node.getName()); + + _namedElementToNodesMapping.put(elem, node); + } + } else { + log.debug("DAG.addNode(): Element already in DAG: {}", node.getName()); + } + + DAGNode parentNode = _namedElementToNodesMapping.get(parent); + DAGEdge edge = new DAGEdge(); + boolean newEdge = _graph.addEdge(parentNode, node, edge); + if (newNode) log.debug("DAG.addNode(): Edge added in DAG: {} --> {} ", parent.getName(), node.getName()); + else log.debug("DAG.addNode(): Edge is already in DAG: {} --> {}", parent.getName(), node.getName()); + + return node; + } + + // ==================================================================================================================================================== + // Remove node method + + public DAGNode removeNode(NamedElement elem) { + if (elem == null) throw new IllegalArgumentException("DAG.removeNode(): Argument cannot be null"); + + // check if children nodes exist + DAGNode node = _namedElementToNodesMapping.get(elem); + if (node == null) { + log.warn("DAG.removeNode(): Element not found (_namedElementToNodesMapping): {}", elem.getName()); + return null; + } + Set edges = _graph.outgoingEdgesOf(node); + if (edges != null && edges.size() > 0) + throw new RuntimeException("Element being removed has children: " + node.getName()); + + // remove node from DAG + _graph.removeVertex(node); // This also removes edges touching this node + _namedElementToNodesMapping.remove(elem); + log.debug("DAG.removeNode(): Element removed from DAG: {}", node.getName()); + + return node; + } + + // ==================================================================================================================================================== + // Add/Remove edge methods + + public DAGEdge addEdge(NamedElement elemFrom, NamedElement elemTo) { + if (elemFrom == null) + throw new IllegalArgumentException("DAG.addEdge(): Argument #1 'elemFrom' cannot be null"); + if (elemTo == null) throw new IllegalArgumentException("DAG.addEdge(): Argument #2 'elemTo' cannot be null"); + + Iterator it = _graph.iterator(); + DAGNode nodeFrom = null; + DAGNode nodeTo = null; + while (it.hasNext() && (nodeFrom == null || nodeTo == null)) { + DAGNode node = it.next(); + if (node.getElement() == elemFrom) nodeFrom = node; + if (node.getElement() == elemTo) nodeTo = node; + } + if (nodeFrom != null && nodeTo != null) { + DAGEdge edge = new DAGEdge(); + boolean newEdge = _graph.addEdge(nodeFrom, nodeTo, edge); + if (newEdge) log.debug("DAG.addEdge(): Edge added in DAG: {} --> {} ", elemFrom.getName(), elemTo.getName()); + else log.debug("DAG.addEdge(): Edge is already in DAG: {} --> {}", elemFrom.getName(), elemTo.getName()); + return edge; + } else { + throw new RuntimeException(String.format("Adding edge FAILED: elem-from=%s -> elem-to=%s. Node not found in DAG: node-from=%s --> node-to=%s", + elemFrom.getName(), elemTo.getName(), (nodeFrom != null ? nodeFrom.getName() : null), (nodeTo != null ? nodeTo.getName() : null))); + } + } + + public DAGEdge addEdge(String elemFrom, String elemTo) { + if (elemFrom == null) + throw new IllegalArgumentException("DAG.addEdge(): Argument #1 'elemFrom' cannot be null"); + if (elemTo == null) throw new IllegalArgumentException("DAG.addEdge(): Argument #2 'elemTo' cannot be null"); + log.debug("DAG.addEdge(): Adding edge in DAG: {} --> {} ", elemFrom, elemTo); + + Iterator it = _graph.iterator(); + DAGNode nodeFrom = null; + DAGNode nodeTo = null; + while (it.hasNext() && (nodeFrom == null || nodeTo == null)) { + DAGNode node = it.next(); + if (elemFrom.equals(node.getName())) nodeFrom = node; + if (elemTo.equals(node.getName())) nodeTo = node; + } + if (nodeFrom != null && nodeTo != null) { + DAGEdge edge = new DAGEdge(); + boolean newEdge = _graph.addEdge(nodeFrom, nodeTo, edge); + if (newEdge) log.debug("DAG.addEdge(): Edge added in DAG: {} --> {} ", elemFrom, elemTo); + else log.debug("DAG.addEdge(): Edge is already in DAG: {} --> {}", elemFrom, elemTo); + return edge; + } else { + throw new RuntimeException(String.format("Adding edge FAILED: elem-from=%s -> elem-to=%s. Node not found in DAG: node-from=%s --> node-to=%s", + elemFrom, elemTo, (nodeFrom != null ? nodeFrom.getName() : null), (nodeTo != null ? nodeTo.getName() : null))); + } + } + + public DAGEdge removeEdge(NamedElement elemFrom, NamedElement elemTo) { + if (elemFrom == null) + throw new IllegalArgumentException("DAG.removeEdge(): Argument #1 'elemFrom' cannot be null"); + if (elemTo == null) throw new IllegalArgumentException("DAG.removeEdge(): Argument #2 'elemTo' cannot be null"); + + Iterator it = _graph.iterator(); + DAGNode nodeFrom = null; + DAGNode nodeTo = null; + while (it.hasNext() && (nodeFrom == null || nodeTo == null)) { + DAGNode node = it.next(); + if (node.getElement() == elemFrom) nodeFrom = node; + if (node.getElement() == elemTo) nodeTo = node; + } + if (nodeFrom != null && nodeTo != null) { + DAGEdge deletedEdge = _graph.removeEdge(nodeFrom, nodeTo); + if (deletedEdge != null) + log.debug("DAG.removeEdge(): Edge removed from DAG: {} --> {} ", elemFrom.getName(), elemTo.getName()); + else log.warn("DAG.removeEdge(): Edge not found in DAG: {} --> {}", elemFrom.getName(), elemTo.getName()); + return deletedEdge; + } else { + throw new RuntimeException(String.format("Removing edge FAILED: elem-from=%s -> elem-to=%s. Node not found in DAG: node-from=%s --> node-to=%s", + elemFrom.getName(), elemTo.getName(), (nodeFrom != null ? nodeFrom.getName() : null), (nodeTo != null ? nodeTo.getName() : null))); + } + } + + public DAGEdge removeEdge(DAGNode nodeFrom, DAGNode nodeTo) { + if (nodeFrom == null) + throw new IllegalArgumentException("DAG.removeEdge(): Argument #1 'nodeFrom' cannot be null"); + if (nodeTo == null) throw new IllegalArgumentException("DAG.removeEdge(): Argument #2 'nodeTo' cannot be null"); + + DAGEdge deletedEdge = _graph.removeEdge(nodeFrom, nodeTo); + if (deletedEdge != null) + log.debug("DAG.removeEdge(): Edge removed from DAG: {} --> {} ", nodeFrom.getElementName(), nodeTo.getElementName()); + else + log.warn("DAG.removeEdge(): Edge not found in DAG: {} --> {}", nodeFrom.getElementName(), nodeTo.getElementName()); + return deletedEdge; + } + + // ==================================================================================================================================================== + // Traverse, query and modify graph methods + + public void traverseDAG(java.util.function.Consumer action) { + log.debug("DAG.traverseDAG(): Traversing graph: Begin"); + _graph.iterator().forEachRemaining(action); + log.debug("DAG.traverseDAG(): Traversing graph: End"); + } + + public Set getAllDAGNodes() { + return _graph.vertexSet(); + } + + public Set getAllDAGEdges() { + return _graph.edgeSet(); + } + + public void clearDAG() { + _graph.removeAllEdges(_graph.edgeSet()); + _graph.removeAllVertices(_graph.vertexSet()); + } + + public void addDAGNode(DAGNode node) { + _graph.addVertex(node); + } + + public void addDAGEdge(DAGNode src, DAGNode trg) { + _graph.addEdge(src, trg); + } + + // ==================================================================================================================================================== + // Export methods + + public String exportToDot() { + if (_graph==null) { + log.warn("DAG.exportToDot(): Cannot export: DAG has not been initialized"); + return null; + } + + DOTExporter exporter = new DOTExporter<>(node -> "NODE_" + node.getId()); + exporter.setVertexAttributeProvider(node -> { + LinkedHashMap vertexAttributes = new LinkedHashMap<>(); + String label; + if (node.getName() != null) { + if (node.getGrouping() != null) { + label = String.format("%s\n[%s]", node.getName(), node.getGrouping()); + } else { + label = node.getName(); + } + } else { + label = ""; + } + // See: https://graphviz.org/doc/info/attrs.html + vertexAttributes.put("label", new DefaultAttribute<>(label, AttributeType.STRING)); + /* + vertexAttributes.put("color", new DefaultAttribute<>("red", AttributeType.STRING)); + vertexAttributes.put("fontcolor", new DefaultAttribute<>("yellow", AttributeType.STRING)); + vertexAttributes.put("fillcolor", new DefaultAttribute<>("cyan:green;0.3", AttributeType.STRING)); + vertexAttributes.put("style", new DefaultAttribute<>("radial", AttributeType.STRING)); + vertexAttributes.put("gradientangle", new DefaultAttribute<>(60, AttributeType.INT)); + */ + return vertexAttributes; + }); + Writer writer = new StringWriter(); + exporter.exportGraph(_graph, writer); + return writer.toString(); + } + + public List exportDAG(String baseFileName, String[] exportFormats, int imageWidth) { + try { + if (!checkExportConfiguration(baseFileName, exportFormats, imageWidth)) return null; + + // Export DAG in DOT format (can be viewd with GraphViz tool) + String dot = exportToDot(); + log.debug("DAG.exportDAG(): Results of exportToDot(): Graph in DOT format:\n{}", dot); + if (dot==null) { + log.warn("DAG.exportDAG(): Cannot export: DAG has not been initialized"); + return null; + } + + // Export DOT into specified formats and save to file(s) + return exportDAG(dot, baseFileName, exportFormats, imageWidth); + + } catch (Exception ex) { + log.error("DAG.exportDAG(): Graph export FAILED: ", ex); + return null; + } + } + + public List exportDAG(@NonNull String dot, String baseFileName, String[] exportFormats, int imageWidth) { + try { + if (!checkExportConfiguration(baseFileName, exportFormats, imageWidth)) return null; + + // Configure Graphviz rendering engine to V8. It's faster + // See also: https://github.com/nidi3/graphviz-java + Graphviz.useEngine(new GraphvizV8Engine()); + + // Export DOT into specified formats and save to file(s) + List exportFilesList = new LinkedList<>(); + MutableGraph mg = new Parser().read(dot); + for (String f : exportFormats) { + Format fmt = Format.valueOf(f.toUpperCase()); + String exportFile = baseFileName + "." + f; + Graphviz.fromGraph(mg).width(imageWidth).render(fmt).toFile(new File(exportFile)); + exportFilesList.add(exportFile); + log.info("DAG.exportDAG(): Graph exported in {} format: {}", fmt, exportFile); + } + return exportFilesList; + + } catch (Exception ex) { + log.error("DAG.exportDAG(): Graph export FAILED: ", ex); + return null; + } + } + + protected boolean checkExportConfiguration(String baseFileName, String[] exportFormats, int imageWidth) { + // check export configuration + if (exportFormats == null || exportFormats.length == 0) { + log.warn("DAG.checkExportConfiguration(): No export formats specified for Graph export: {}", Arrays.toString(exportFormats)); + return false; + } + if (imageWidth < 1) { + log.warn("DAG.checkExportConfiguration(): Invalid image width for Graph export: {}", imageWidth); + return false; + } + return true; + } + + public String toString() { + return _graph!=null ? _graph.toString() : null; + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAGEdge.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAGEdge.java new file mode 100644 index 0000000..7c194f8 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAGEdge.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.dag; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jgrapht.graph.DefaultEdge; + +import java.util.concurrent.atomic.AtomicLong; + +@RequiredArgsConstructor +public class DAGEdge extends DefaultEdge { + private final static AtomicLong edgeCounter = new AtomicLong(); + + @Getter + private final long id; + + public DAGEdge() { + id = edgeCounter.getAndIncrement(); + } + + public DAGNode getSource() { + return (DAGNode) super.getSource(); + } + + public DAGNode getTarget() { + return (DAGNode) super.getTarget(); + } + + public int hashCode() { + return toString().hashCode(); + } + + public boolean equals(Object o) { + return (o instanceof DAGEdge) && (toString().equals(o.toString())); + } + + public String toString() { + return "EDGE #" + id; + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAGNode.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAGNode.java new file mode 100644 index 0000000..336a308 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/dag/DAGNode.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.dag; + +import gr.iccs.imu.ems.translate.Grouping; +import gr.iccs.imu.ems.translate.model.NamedElement; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.io.Serializable; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +@Getter +@RequiredArgsConstructor +public class DAGNode implements Serializable { + private final static AtomicLong counter = new AtomicLong(); + + private final long id; + private final String name; + private final NamedElement element; + private final String elementName; + private Grouping grouping; + + DAGNode() { + id = counter.getAndIncrement(); + element = null; + elementName = null; + name = null; + } + + public DAGNode(@NonNull NamedElement elem, @NonNull String fullName) { + id = counter.getAndIncrement(); + element = elem; + elementName = element.getName(); + name = fullName; + } + + public DAGNode setGrouping(Grouping g) { + grouping = g; + return this; + } + + public int hashCode() { + return toString().hashCode(); + } + + public boolean equals(Object o) { + return (o instanceof DAGNode) && (toString().equals(o.toString())); + } + + public String toString() { + return "NODE "+ Objects.requireNonNullElse(name, ""); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/AbstractInterfaceRootObject.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/AbstractInterfaceRootObject.java new file mode 100644 index 0000000..32a3e89 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/AbstractInterfaceRootObject.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.util.Map; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public abstract class AbstractInterfaceRootObject extends AbstractRootObject { + protected Map additionalProperties; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/AbstractRootObject.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/AbstractRootObject.java new file mode 100644 index 0000000..2f503da --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/AbstractRootObject.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +public abstract class AbstractRootObject implements Serializable { + @JsonProperty("@objectClass") + private final String _objectClass = getClass().getName(); + + @JsonIgnore + protected transient Object object; + protected NamedElement container; + + public T getObject(Class c) { + return c.cast(object); + } + + public T getContainer(Class c) { + return c.cast(container); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Action.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Action.java new file mode 100644 index 0000000..4e9b558 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Action.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Action extends Feature { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Annotation.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Annotation.java new file mode 100644 index 0000000..78e7111 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Annotation.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Annotation extends AbstractRootObject { + protected String id; + protected String uri; + protected boolean implemented; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Attribute.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Attribute.java new file mode 100644 index 0000000..1e585ff --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Attribute.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Attribute extends NamedElement { + private Object value; + private ValueType valueType; + private String unit; + private Object minValue; + private Object maxValue; + private boolean minInclusive; + private boolean maxInclusive; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BinaryEventPattern.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BinaryEventPattern.java new file mode 100644 index 0000000..3c2a5e6 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BinaryEventPattern.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class BinaryEventPattern extends EventPattern { + private Event leftEvent; + private Event rightEvent; + private double lowerOccurrenceBound; + private double upperOccurrenceBound; + private BinaryPatternOperatorType operator; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BinaryPatternOperatorType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BinaryPatternOperatorType.java new file mode 100644 index 0000000..9861f81 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BinaryPatternOperatorType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum BinaryPatternOperatorType { + AND, OR, XOR, PRECEDES, REPEAT_UNTIL +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BooleanValue.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BooleanValue.java new file mode 100644 index 0000000..7e34bd9 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/BooleanValue.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class BooleanValue extends Value { + private boolean value; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ComparisonOperatorType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ComparisonOperatorType.java new file mode 100644 index 0000000..6d1696e --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ComparisonOperatorType.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ComparisonOperatorType { + GREATER_THAN("GREATER_THAN", ">"), + GREATER_EQUAL_THAN("GREATER_EQUAL_THAN", ">="), + LESS_THAN("LESS_THAN", "<"), + LESS_EQUAL_THAN("LESS_EQUAL_THAN", "<="), + EQUAL("EQUAL", "="), + NOT_EQUAL("NOT_EQUAL", "<>"); + + private final String name; + private final String operator; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Component.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Component.java new file mode 100644 index 0000000..0be04c8 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Component.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Component extends Feature { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeConstraint.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeConstraint.java new file mode 100644 index 0000000..bd6c393 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeConstraint.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CompositeConstraint extends Constraint { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeMetric.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeMetric.java new file mode 100644 index 0000000..09af481 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeMetric.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CompositeMetric extends Metric { + private String formula; + @Builder.Default + private List componentMetrics = new ArrayList<>(); + + public boolean containsMetric(Metric m) { + return componentMetrics.contains(m); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeMetricContext.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeMetricContext.java new file mode 100644 index 0000000..2419b25 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CompositeMetricContext.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CompositeMetricContext extends MetricContext { + private GroupingType groupingType; + @Builder.Default + private List composingMetricContexts = new ArrayList<>(); + private Window window; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Constraint.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Constraint.java new file mode 100644 index 0000000..7639a7e --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Constraint.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Constraint extends NamedElement { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CriterionType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CriterionType.java new file mode 100644 index 0000000..66f6d62 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/CriterionType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum CriterionType { + INSTANCE, HOST, ZONE, REGION, CLOUD, TIMESTAMP, CUSTOM +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Data.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Data.java new file mode 100644 index 0000000..c1fcf9b --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Data.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder() +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Data extends Feature { + private DataSource dataSource; + @Builder.Default + private List includedData = new ArrayList<>(); +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/DataSource.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/DataSource.java new file mode 100644 index 0000000..9f0cbc9 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/DataSource.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DataSource extends Feature { + private boolean external; + private Component component; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/DoubleValue.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/DoubleValue.java new file mode 100644 index 0000000..f63e2bb --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/DoubleValue.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DoubleValue extends NumericValue { + private double value; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Event.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Event.java new file mode 100644 index 0000000..2eeeaaf --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Event.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Event extends Feature { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/EventPattern.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/EventPattern.java new file mode 100644 index 0000000..4e74576 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/EventPattern.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class EventPattern extends Event { + private Timer timer; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Feature.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Feature.java new file mode 100644 index 0000000..98582f4 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Feature.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Feature extends NamedElement { + protected List attributes; + protected List subFeatures; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/FloatValue.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/FloatValue.java new file mode 100644 index 0000000..5686da2 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/FloatValue.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class FloatValue extends NumericValue { + private float value; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Function.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Function.java new file mode 100644 index 0000000..d57ba2f --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Function.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Function extends Feature { + private String expression; + @Builder.Default + private List arguments = new ArrayList<>(); +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/GroupingType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/GroupingType.java new file mode 100644 index 0000000..8c23c9a --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/GroupingType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum GroupingType { + PER_INSTANCE, PER_HOST, PER_ZONE, PER_REGION, PER_CLOUD, GLOBAL +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/IfThenConstraint.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/IfThenConstraint.java new file mode 100644 index 0000000..7f7cd55 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/IfThenConstraint.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class IfThenConstraint extends CompositeConstraint { + private Constraint ifConstraint; + private Constraint thenConstraint; + private Constraint elseConstraint; + + public Constraint getIf() { + return ifConstraint; + } + + public Constraint getThen() { + return thenConstraint; + } + + public Constraint getElse() { + return elseConstraint; + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/IntValue.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/IntValue.java new file mode 100644 index 0000000..f534049 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/IntValue.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class IntValue extends NumericValue { + private int value; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Interval.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Interval.java new file mode 100644 index 0000000..0680763 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Interval.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +// See: eu.melodic.models.interfaces.Interval +public class Interval extends AbstractInterfaceRootObject { + @ToString + public enum UnitType { DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS } + + private UnitType unit; + private int period; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LoadMetricVariable.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LoadMetricVariable.java new file mode 100644 index 0000000..f6eb633 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LoadMetricVariable.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class LoadMetricVariable extends MetricVariable { + public LoadMetricVariable(String name, MetricContext context) { + setName(name); + setMetricContext(context); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LogicalConstraint.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LogicalConstraint.java new file mode 100644 index 0000000..ff41a76 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LogicalConstraint.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class LogicalConstraint extends CompositeConstraint { + private LogicalOperatorType logicalOperator; + @Builder.Default + private List constraints = new ArrayList<>(); + + public boolean containsConstraint(@NonNull Constraint c) { + return constraints.contains(c); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LogicalOperatorType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LogicalOperatorType.java new file mode 100644 index 0000000..4ce6f13 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/LogicalOperatorType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum LogicalOperatorType { + AND, OR, XOR +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MeasurableAttribute.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MeasurableAttribute.java new file mode 100644 index 0000000..034888c --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MeasurableAttribute.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MeasurableAttribute extends Attribute { + @Builder.Default + private List sensors = new ArrayList<>(); +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Metric.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Metric.java new file mode 100644 index 0000000..1dfe13a --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Metric.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Metric extends Feature { + protected MetricTemplate metricTemplate; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricConstraint.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricConstraint.java new file mode 100644 index 0000000..cd45f32 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricConstraint.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MetricConstraint extends UnaryConstraint { + private MetricContext metricContext; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricContext.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricContext.java new file mode 100644 index 0000000..3198c75 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricContext.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MetricContext extends Feature { + private Metric metric; + private Schedule schedule; + private ObjectContext objectContext; + + public String getComponentName() { + if (objectContext==null) return null; + if (objectContext.getComponent()!=null) return objectContext.getComponent().getName(); + return null; + } + + public String getDataName() { + if (objectContext==null) return null; + if (objectContext.getData()!=null) return objectContext.getData().getName(); + return null; + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricTemplate.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricTemplate.java new file mode 100644 index 0000000..a7d9487 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricTemplate.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MetricTemplate extends Feature { + private ValueType valueType; + private short valueDirection; + private String unit; + private MeasurableAttribute attribute; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricVariable.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricVariable.java new file mode 100644 index 0000000..974df1c --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricVariable.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MetricVariable extends Metric { + private boolean currentConfiguration; + private Component component; + private boolean onNodeCandidates; + private String formula; + @Builder.Default + private List componentMetrics = new ArrayList<>(); + private MetricContext metricContext; + + public boolean containsMetric(Metric m) { + return componentMetrics.contains(m); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricVariableConstraint.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricVariableConstraint.java new file mode 100644 index 0000000..2e882ce --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/MetricVariableConstraint.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MetricVariableConstraint extends UnaryConstraint { + private MetricVariable metricVariable; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Monitor.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Monitor.java new file mode 100644 index 0000000..74655be --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Monitor.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.List; +import java.util.Map; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +// See: eu.melodic.models.interfaces.Monitor +public class Monitor extends AbstractInterfaceRootObject { + private String metric; + private String component; + private Sensor sensor; + private List sinks; + private Map tags; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NamedElement.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NamedElement.java new file mode 100644 index 0000000..03d8da2 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NamedElement.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class NamedElement extends AbstractRootObject { + protected String name; + protected String description; + protected List annotations; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NonFunctionalEvent.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NonFunctionalEvent.java new file mode 100644 index 0000000..16d6e19 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NonFunctionalEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class NonFunctionalEvent extends SingleEvent { + private MetricConstraint metricConstraint; + private boolean isViolation; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NumericValue.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NumericValue.java new file mode 100644 index 0000000..03ba42a --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/NumericValue.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class NumericValue extends Value { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ObjectContext.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ObjectContext.java new file mode 100644 index 0000000..b4614d3 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ObjectContext.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ObjectContext extends Feature { + private Component component; + private Data data; + //private Communication communication; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/OptimisationRequirement.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/OptimisationRequirement.java new file mode 100644 index 0000000..67f98dd --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/OptimisationRequirement.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class OptimisationRequirement extends Requirement { // SoftRequirement + private double priority; + private MetricContext metricContext; + private MetricVariable metricVariable; + private boolean minimise; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/PullSensor.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/PullSensor.java new file mode 100644 index 0000000..bc06dbb --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/PullSensor.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.HashMap; +import java.util.Map; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +// Based on: eu.melodic.models.interfaces.PullSensor +public class PullSensor extends Sensor { + private String className; + @Builder.Default + private Map configuration = new HashMap<>(); + private Interval interval; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/PushSensor.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/PushSensor.java new file mode 100644 index 0000000..b0de16d --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/PushSensor.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +// Based on: eu.melodic.models.interfaces.PushSensor +public class PushSensor extends Sensor { + private int port; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/RawMetric.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/RawMetric.java new file mode 100644 index 0000000..d8d5fe2 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/RawMetric.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class RawMetric extends Metric { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/RawMetricContext.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/RawMetricContext.java new file mode 100644 index 0000000..8edb825 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/RawMetricContext.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class RawMetricContext extends MetricContext { + private Sensor sensor; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Requirement.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Requirement.java new file mode 100644 index 0000000..6cb3588 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Requirement.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Requirement extends Feature { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ScalabilityRule.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ScalabilityRule.java new file mode 100644 index 0000000..9ba36de --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ScalabilityRule.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ScalabilityRule extends Feature { + private Event event; + @Builder.Default + private List actions = new ArrayList<>(); +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ScalingAction.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ScalingAction.java new file mode 100644 index 0000000..cc8df30 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ScalingAction.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ScalingAction extends Action { + private Component component; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Schedule.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Schedule.java new file mode 100644 index 0000000..d74f3cf --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Schedule.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Schedule extends Feature { + private Date start; + private Date end; + private String timeUnit; + private long interval; + private int repetitions; + + public long getIntervalInMillis() { + if (timeUnit == null) return interval; + return TimeUnit.MILLISECONDS.convert(interval, TimeUnit.valueOf(timeUnit.toUpperCase())); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Sensor.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Sensor.java new file mode 100644 index 0000000..b037f24 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Sensor.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Sensor extends Component { + private String configurationStr; + private boolean isPush; + + public Map additionalProperties; + + public boolean isPullSensor() { + return !isPush; + } + + public boolean isPushSensor() { + return isPush; + } + + public PullSensor pullSensor() { + if (this instanceof PullSensor) + return (PullSensor) this; + throw new IllegalArgumentException("Not a Pull sensor: " + this.getName()); + } + + public PushSensor pushSensor() { + if (this instanceof PushSensor) + return (PushSensor) this; + throw new IllegalArgumentException("Not a Push sensor: " + this.getName()); + } +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ServiceLevelObjective.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ServiceLevelObjective.java new file mode 100644 index 0000000..66f6861 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ServiceLevelObjective.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ServiceLevelObjective extends Requirement { // HardRequirement + private Constraint constraint; + private Event violationEvent; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/SingleEvent.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/SingleEvent.java new file mode 100644 index 0000000..a6f50be --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/SingleEvent.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SingleEvent extends Feature { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Sink.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Sink.java new file mode 100644 index 0000000..8d9d805 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Sink.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.HashMap; +import java.util.Map; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +// See: eu.melodic.models.interfaces.Sink +public class Sink extends AbstractInterfaceRootObject { + public enum Type { /*CLI, KAIROS_DB,*/ INFLUX, JMS } + + private Type type; + private String component; + @Builder.Default + private Map configuration = new HashMap<>(); +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/StringValue.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/StringValue.java new file mode 100644 index 0000000..e849300 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/StringValue.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class StringValue extends Value { + private String value; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Timer.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Timer.java new file mode 100644 index 0000000..65e5f03 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Timer.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Timer extends Feature { + private TimerType type; + private int timeValue; + private int maxOccurrenceNum; + private String unit; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/TimerType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/TimerType.java new file mode 100644 index 0000000..457369c --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/TimerType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum TimerType { + WITHIN, WITHIN_MAX, INTERVAL +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryConstraint.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryConstraint.java new file mode 100644 index 0000000..6af266b --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryConstraint.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Date; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UnaryConstraint extends Constraint { + private Date validity; + private ComparisonOperatorType comparisonOperator; + private double threshold; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryEventPattern.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryEventPattern.java new file mode 100644 index 0000000..4bba0df --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryEventPattern.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UnaryEventPattern extends EventPattern { + private Event event; + private double occurrenceNum; + private UnaryPatternOperatorType operator; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryPatternOperatorType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryPatternOperatorType.java new file mode 100644 index 0000000..7d777ea --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/UnaryPatternOperatorType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum UnaryPatternOperatorType { + EVERY, NOT, REPEAT, WHEN +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Value.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Value.java new file mode 100644 index 0000000..d14c1a4 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Value.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Value extends AbstractRootObject { +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ValueType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ValueType.java new file mode 100644 index 0000000..10307fb --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/ValueType.java @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum ValueType { + //getPrimitiveType + INT_TYPE, STRING_TYPE, BOOLEAN_TYPE, FLOAT_TYPE, DOUBLE_TYPE, + IntType, StringType, BooleanType, FloatType, DoubleType +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/VariableType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/VariableType.java new file mode 100644 index 0000000..4885840 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/VariableType.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum VariableType { + CPU("cpu"), + CORES("cores"), + RAM("ram"), + STORAGE("storage"), + PROVIDER("provider"), + CARDINALITY("cardinality"), + OS("os"), + LOCATION("location"), + LATITUDE("latitude"), + LONGITUDE("longitude"); + + private final String name; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Window.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Window.java new file mode 100644 index 0000000..c75b7ca --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/Window.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Window extends Feature { + private String timeUnit; + private WindowType windowType; + private WindowSizeType sizeType; + private long measurementSize; + private long timeSize; + @Builder.Default + private List processings = null; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowCriterion.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowCriterion.java new file mode 100644 index 0000000..cb5ee4b --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowCriterion.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WindowCriterion extends NamedElement { + private Metric metric; + private CriterionType type; + private String custom; + private boolean ascending; +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowProcessing.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowProcessing.java new file mode 100644 index 0000000..94c89d1 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowProcessing.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.ArrayList; +import java.util.List; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WindowProcessing extends Feature { + private WindowProcessingType processingType; + @Builder.Default + private List groupingCriteria = new ArrayList<>(); + @Builder.Default + private List rankingCriteria = new ArrayList<>(); +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowProcessingType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowProcessingType.java new file mode 100644 index 0000000..ce84845 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowProcessingType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum WindowProcessingType { + GROUP, SORT, RANK +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowSizeType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowSizeType.java new file mode 100644 index 0000000..d378804 --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowSizeType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum WindowSizeType { + MEASUREMENTS_ONLY, TIME_ONLY, FIRST_MATCH, BOTH_MATCH, TIME_ACCUM, TIME_ORDER +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowType.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowType.java new file mode 100644 index 0000000..d1064bf --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/model/WindowType.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.model; + +public enum WindowType { + FIXED, SLIDING +} diff --git a/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/mvv/MetricVariableValuesService.java b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/mvv/MetricVariableValuesService.java new file mode 100644 index 0000000..a810cfe --- /dev/null +++ b/ems-core/translator/src/main/java/gr/iccs/imu/ems/translate/mvv/MetricVariableValuesService.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.translate.mvv; + +import gr.iccs.imu.ems.translate.TranslationContext; + +import java.util.Map; +import java.util.Set; + +public interface MetricVariableValuesService { + void init(); + Map getMatchingMetricVariableValues(String cpModelPath, TranslationContext _TC); + Map getMetricVariableValues(String cpModelPath, Set variableNames); +} diff --git a/ems-core/translator/src/main/resources/banner-alternative.txt b/ems-core/translator/src/main/resources/banner-alternative.txt new file mode 100644 index 0000000..80fb379 --- /dev/null +++ b/ems-core/translator/src/main/resources/banner-alternative.txt @@ -0,0 +1,7 @@ + ______ __ ___ __________ __ + / ____/___ _____ ___ ___ / / |__ \ / ____/ __ \/ / + / / / __ `/ __ `__ \/ _ \/ / __/ / / __/ / /_/ / / +/ /___/ /_/ / / / / / / __/ / / __/ / /___/ ____/ /___ +\____/\__,_/_/ /_/ /_/\___/_/ /____/ /_____/_/ /_____/ + + \ No newline at end of file diff --git a/ems-core/translator/src/main/resources/banner.txt b/ems-core/translator/src/main/resources/banner.txt new file mode 100644 index 0000000..efd1f4e --- /dev/null +++ b/ems-core/translator/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _____ _ ___ ______ _____ _ + / ____| | | |__ \ | ____| __ \| | + | | __ _ _ __ ___ ___| | ) | | |__ | |__) | | + | | / _` | '_ ` _ \ / _ \ | / / | __| | ___/| | + | |___| (_| | | | | | | __/ | / /_ | |____| | | |____ + \_____\__,_|_| |_| |_|\___|_| |____| |______|_| |______| + + \ No newline at end of file diff --git a/ems-core/translator/src/main/resources/rule-templates.yml b/ems-core/translator/src/main/resources/rule-templates.yml new file mode 100644 index 0000000..65d85d3 --- /dev/null +++ b/ems-core/translator/src/main/resources/rule-templates.yml @@ -0,0 +1,237 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +# EPL Rule templates per Element-Type and Monitoring-Grouping + +DOLLAR: '$' +translator.generator: + language: EPL + rule-templates: + # SCHEDULE (i.e. IUTPUT) CLAUSE + SCHEDULE: + __ANY__: + - | + OUTPUT ALL EVERY [(${DOLLAR}{period})] [(${DOLLAR}{unit})] + AGG: + - | + OUTPUT SNAPSHOT EVERY [(${DOLLAR}{period})] [(${DOLLAR}{unit})] + # Binary-Event-Pattern templates + BEP-AND: + GLOBAL: + - | + /* BEP-AND-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT le.* FROM [(${DOLLAR}{leftEvent})].std:lastevent() AS le, [(${DOLLAR}{rightEvent})].std:lastevent() AS re + BEP-OR: + GLOBAL: +#XXX: TEST: + - | + /* BEP-OR-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT CASE WHEN le IS NULL THEN re ELSE le END AS evt FROM PATTERN [ EVERY ( le=[(${DOLLAR}{leftEvent})] OR re=[(${DOLLAR}{rightEvent})] ) ] + BEP-XOR: +#XXX: XOR is NOT SUPPORTED: IS IT EQUIVALENT TO OR?? + GLOBAL: +#XXX: TEST: + - | + /* BEP-XOR-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT CASE WHEN le IS NULL THEN re ELSE le END AS evt FROM PATTERN [ EVERY ( le=[(${DOLLAR}{leftEvent})] OR re=[(${DOLLAR}{rightEvent})] ) ] + BEP-PRECEDES: + GLOBAL: +#XXX: TEST: + - | + /* BEP-PRECEDES-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT le.* FROM PATTERN [ EVERY ( le=[(${DOLLAR}{leftEvent})] -> re=[(${DOLLAR}{rightEvent})] ) ] + BEP-REPEAT_UNTIL: + GLOBAL: +#XXX: TEST: + - | + /* BEP-REPEAT_UNTIL-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT re.* FROM PATTERN [ EVERY [ le=[(${DOLLAR}{leftEvent})] UNTIL re=[(${DOLLAR}{rightEvent})] ] ] WHERE le IS NOT NULL + + # Unary-Event-Pattern templates + UEP-EVERY: +#XXX: WHAT'S THE MEANING OF THIS OPERATOR?? ...IF STANDALONE?? + GLOBAL: + - | + /* UEP-EVERY-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT ue.* FROM PATTERN [ EVERY ue=[(${DOLLAR}{unaryEvent})] ] + UEP-NOT: +#XXX: WHAT'S THE MEANING OF THIS OPERATOR?? ...IF STANDALONE?? + GLOBAL: + - | + /* UEP-NOT-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT ue.* FROM PATTERN [ NOT ue=[(${DOLLAR}{unaryEvent})] ] + UEP-REPEAT: + GLOBAL: +#XXX: TEST: + - | + /* UEP-REPEAT-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT ue[0].* FROM PATTERN [ [[(${DOLLAR}{occurrenceNum})]] ue=[(${DOLLAR}{unaryEvent})] ] + UEP-WHEN: +#XXX: WHAT'S THE MEANING OF THIS OPERATOR?? ...IF STANDALONE?? + GLOBAL: + - | + /* UEP-WHEN-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT ue.* FROM [(${DOLLAR}{leftEvent})].std:lastevent() AS ue + + # Non-Functional-Event templates + NFE: + GLOBAL: + - | + /* NFE-GLOBAL */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT * FROM [(${DOLLAR}{metricConstraint})].std:lastevent() + + # Metric-Constraint templates + CONSTR-MET: + __ANY__: + - | + /* CONSTR-MET-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT * FROM [(${DOLLAR}{metricContext})] HAVING [(${DOLLAR}{metricContext})].metricValue [(${DOLLAR}{operator})] [(${DOLLAR}{threshold})] + + # Logical-Constraint templates + CONSTR-LOG: + __ANY__: + - | + /* CONSTR-LOG-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT 1 AS metricValue, 3 AS level, current_timestamp AS timestamp + FROM PATTERN [ EVERY ( [# th:each="con,iterStat : ${DOLLAR}{constraints}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{con} + ' '+${DOLLAR}{operator}+' ' : ${DOLLAR}{con}"] [/] ) ] + + # If-Then-Constraint templates + CONSTR-IF-THEN: + __ANY__: +#XXX: TEST: + - | + /* CONSTR-IF-THEN-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT 1 AS metricValue, 3 AS level, current_timestamp AS timestamp + FROM PATTERN [ EVERY ( [(${DOLLAR}{ifConstraint})] AND [(${DOLLAR}{thenConstraint})] [# th:if="${DOLLAR}{elseConstraint != null}" th:text="'OR NOT ( ' + ${DOLLAR}{ifConstraint} + ' ) AND ' + ${DOLLAR}{elseConstraint}"] [/] ) ] + + # Context templates + COMP-CTX: + __ANY__: + - | + /* COMP-CTX-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ [# th:switch="${DOLLAR}{selectMode}"] [# th:case="'epl'"] + SELECT [(${DOLLAR}{formula})] [/] [# th:case="*"] + SELECT EVAL( '[(${DOLLAR}{formula})]', '[# th:each="ctx,iterStat : ${DOLLAR}{components}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ',' : ${DOLLAR}{ctx}"] [/]', [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}"] [/] ) AS metricValue, + 3 AS level, + current_timestamp AS timestamp [/] [/] + FROM [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:utext="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx}+${DOLLAR}{windowClause}+' AS '+${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}+${DOLLAR}{windowClause}+' AS '+${DOLLAR}{ctx}"] [/] + [(${DOLLAR}{scheduleClause})] + + AGG-COMP-CTX: + __ANY__: + - | + /* COMP-CTX-AGG-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ [# th:switch="${DOLLAR}{selectMode}"] [# th:case="'epl'"] + SELECT [(${DOLLAR}{formula})] [/] [# th:case="*"] + SELECT EVALAGG( '[(${DOLLAR}{formula})]', '[# th:each="ctx,iterStat : ${DOLLAR}{components}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ',' : ${DOLLAR}{ctx}"] [/]', [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}"] [/] ) AS metricValue, + 3 AS level, + current_timestamp AS timestamp [/] [/] + FROM [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:utext="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx}+${DOLLAR}{windowClause}+' AS '+${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}+${DOLLAR}{windowClause}+' AS '+${DOLLAR}{ctx}"] [/] + [(${DOLLAR}{scheduleClause})] + + RAW-CTX: + PER_INSTANCE: + - | + /* RAW-CTX-PER_INSTANCE */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT * FROM [(${DOLLAR}{sensor})] [(${DOLLAR}{scheduleClause})] + + # Metric templates + TL-MET: + __ANY__: + - | + /* MET-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT * FROM [(${DOLLAR}{context})] + + # Metric Variable templates + VAR: + __ANY__: + - | + /* VAR-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ [# th:switch="${DOLLAR}{selectMode}"] [# th:case="'epl'"] + SELECT [(${DOLLAR}{formula})] [/] [# th:case="*"] + SELECT EVAL( '[(${DOLLAR}{formula})]', '[# th:each="ctx,iterStat : ${DOLLAR}{components}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ',' : ${DOLLAR}{ctx}"] [/]', [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}"] [/] ) AS metricValue, + 3 AS level, + current_timestamp AS timestamp [/] [/] + FROM [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx}+' AS '+${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}+' AS '+${DOLLAR}{ctx}"] [/] + + AGG-VAR: + __ANY__: + - | + /* VAR-AGG-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ [# th:switch="${DOLLAR}{selectMode}"] [# th:case="'epl'"] + SELECT [(${DOLLAR}{formula})] [/] [# th:case="*"] + SELECT EVALAGG( '[(${DOLLAR}{formula})]', '[# th:each="ctx,iterStat : ${DOLLAR}{components}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ',' : ${DOLLAR}{ctx}"] [/]', [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}"] [/] ) AS metricValue, + 3 AS level, + current_timestamp AS timestamp [/] [/] + FROM [# th:each="ctx,iterStat : ${DOLLAR}{contexts}" th:text="!${DOLLAR}{iterStat.last} ? ${DOLLAR}{ctx}+' AS '+${DOLLAR}{ctx} + ', ' : ${DOLLAR}{ctx}+' AS '+${DOLLAR}{ctx}"] [/] + + LOAD-VAR: + __ANY__: + - | + /* LOAD-VAR-any */ /*INSERT INTO [(${outputStream})]*/ + SELECT * FROM [(${context})] + + # Optimisation-Requirement templates + OPT-REQ-CTX: + __ANY__: + - | + /* OPT-REQ-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT * FROM [(${DOLLAR}{context})] + + OPT-REQ-VAR: + __ANY__: + - | + /* OPT-REQ-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT * FROM [(${DOLLAR}{variable})] + + # SLO templates + SLO: + __ANY__: + - | + /* SLO-any */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT * FROM [(${DOLLAR}{constraint})] + +#XXX:DEL:...remove next rule + XXX-extra-rule-templates: + BEP-AND: + GLOBAL: + - | + /* BEP-AND-GLOBAL : ALTERNATIVE */ /*INSERT INTO [(${DOLLAR}{outputStream})]*/ + SELECT le.* FROM PATTERN [ EVERY (le=[(${DOLLAR}{leftEvent})] AND re=[(${DOLLAR}{rightEvent})]) ] + RAW-CTX: + PER_INSTANCE: + - | + /* RAW-CTX-PER_INSTANCE */ + INSERT INTO TEST_STREAM + SELECT EVAL('-1*CPUMetric+CPUMetric_2+CPUMetric_3', '[(${DOLLAR}{metric})],[(${DOLLAR}{metric})]_2,[(${DOLLAR}{metric})]_3', [(${DOLLAR}{metric})], [(${DOLLAR}{metric})]_2, [(${DOLLAR}{metric})]_3) AS metricValue, + 1 AS level, + current_timestamp AS timestamp + FROM [(${DOLLAR}{metric})] as [(${DOLLAR}{metric})], [(${DOLLAR}{metric})] as [(${DOLLAR}{metric})]_2, [(${DOLLAR}{metric})] as [(${DOLLAR}{metric})]_3[(${DOLLAR}{scheduleClause})] + + FE: + PER_INSTANCE: + - | + /* XXX: TODO: FE-PER_INSTANCE */ + .......... Functional Event + CONSTR-IF-THEN: + PER_INSTANCE: + - | + /* XXX: TODO: CONSTR-IF-THEN-PER_INSTANCE */ + .......... If-Then constraint + CONSTR-VAR: + PER_INSTANCE: + - | + /* XXX: TODO: CONSTR-VAR-PER_INSTANCE */ + .......... Metric Variable constraint + CONSTR-LOG: + PER_INSTANCE: + - | + /* XXX: TODO: CONSTR-LOG-PER_INSTANCE */ + .......... Logical constraint + VAR: + PER_INSTANCE: + - | + /* XXX: TODO: VAR-PER_INSTANCE */ + .......... Metric Variable \ No newline at end of file diff --git a/ems-core/util/pom.xml b/ems-core/util/pom.xml new file mode 100644 index 0000000..f6c7658 --- /dev/null +++ b/ems-core/util/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + util + EMS - Utilities + + + + + org.springframework.boot + spring-boot-starter + + + + + org.projectlombok + lombok + provided + + + + + com.google.code.gson + gson + + + + + org.apache.commons + commons-text + + + + + org.cryptacular + cryptacular + 1.2.5 + + + + + org.bouncycastle + bcpg-jdk18on + compile + + + org.bouncycastle + bcpkix-jdk18on + compile + + + org.bouncycastle + bcprov-jdk18on + + + + + + + + maven-assembly-plugin + + + + gr.iccs.imu.ems.util.NetUtil + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/ClientConfiguration.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/ClientConfiguration.java new file mode 100644 index 0000000..ae258c6 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/ClientConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.*; + +import java.io.Serializable; +import java.util.Set; + +/** + * Baguette Client Configuration + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ClientConfiguration implements Serializable { + @NonNull private Set nodesWithoutClient; +} \ No newline at end of file diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/CredentialsMap.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/CredentialsMap.java new file mode 100644 index 0000000..d4c410c --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/CredentialsMap.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import gr.iccs.imu.ems.util.password.PasswordEncoder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.stream.Collectors; + +/** + * CredentialsMap is a HashMap for storing username/passwords in-memory. + * It includes a preferred key (i.e. username), and overrides 'toString()' method in order to password-encode entry values. + */ +@Slf4j +public class CredentialsMap extends HashMap { + @Getter + private PasswordEncoder passwordEncoder; + @Getter + private String preferredKey; + + public CredentialsMap() { + this(PasswordUtil.getDefaultPasswordEncoder()); + } + + public CredentialsMap(PasswordEncoder pe) { + this.passwordEncoder = pe; + } + + public String put(String key, String value, boolean preferred) { + if (preferred) preferredKey = key; + return super.put(key, value); + } + + public String remove(String key) { + if (key.equals(preferredKey)) preferredKey = null; + return super.remove(key); + } + + public boolean hasPreferredPair() { + return preferredKey!=null; + } + + public CredentialsMap.Entry getPreferredPair() { + if (preferredKey==null) return null; + return new CredentialsMap.SimpleEntry<>(preferredKey, get(preferredKey)); + } + + public String toString() { + return entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, e -> passwordEncoder.encode(e.getValue()))) + .toString(); + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/CredentialsMapConverter.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/CredentialsMapConverter.java new file mode 100644 index 0000000..4614934 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/CredentialsMapConverter.java @@ -0,0 +1,49 @@ + +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import com.google.gson.Gson; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +/** + * Converts a String to a CredentialsMap + */ +@Slf4j +@Component +@ConfigurationPropertiesBinding +public class CredentialsMapConverter implements Converter { + private Gson gson; + + public CredentialsMapConverter() { + gson = new Gson(); + } + + @Override + public CredentialsMap convert(@NonNull String s) { + if (StringUtils.isNotBlank(s)) { + try { + CredentialsMap credentialsMap = gson.fromJson(s.trim(), CredentialsMap.class); + log.debug("CredentialsMapConverter: result: {}", credentialsMap); + return credentialsMap; + } catch (Throwable t) { + log.debug("CredentialsMapConverter: JSON input: {}", s); + log.error("CredentialsMapConverter: EXCEPTION while parsing JSON input: ", t); + throw new IllegalArgumentException(t); + } + } + throw new IllegalArgumentException("Input is blank: "+s); + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/EmsConstant.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/EmsConstant.java new file mode 100644 index 0000000..af6f6b6 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/EmsConstant.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +/** + * EMS constant + */ +public class EmsConstant { + public final static String EMS_PROPERTIES_PREFIX = ""; //""ems."; + public final static String EVENT_PROPERTY_SOURCE_ADDRESS = "producer-host"; + public final static String EVENT_PROPERTY_ORIGINAL_DESTINATION = "original-destination"; + public final static String EVENT_PROPERTY_EFFECTIVE_DESTINATION = "effective-destination"; + + public final static String COLLECTOR_DESTINATION_ALIASES = "destination-aliases"; + public final static String COLLECTOR_DESTINATION_ALIASES_DELIMITERS = "[,;: \t\r\n]+"; + public final static String COLLECTOR_ALLOWED_TOPICS_VAR = "COLLECTOR_ALLOWED_TOPICS"; +} \ No newline at end of file diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/EventBus.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/EventBus.java new file mode 100644 index 0000000..bd82175 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/EventBus.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.Builder; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.regex.Pattern; + +@Slf4j +@Builder +public class EventBus { // Topic,Message,Sender + //enum STANDARD_EVENT_TOPICS { CONTROL_EVENT, TRANSLATOR_EVENT, BAGUETTE_SERVER_EVENT, BROKER_CEP_EVENT, CLIENT_INSTALLER_EVENT } + + /*private static EventBus _DEFAULT; + private static void initDefault() { + _DEFAULT = EventBus.builder() + //.allowedTopics(Arrays.stream(STANDARD_EVENT_TOPICS.values()).map(Enum::name).collect(Collectors.toSet())) + .build(); + } + public static EventBus getDefault() { initDefault(); return _DEFAULT; } + public static void setDefault(EventBus eventBus) { _DEFAULT = eventBus; }*/ + + private final Set allowedTopics; + private final Set allowedSenders; + private final Map>> topicsAndConsumers = new LinkedHashMap<>(); + private final Map, List> consumerPatternMap = new LinkedHashMap<>(); + + public void send(@NonNull T topic, @NonNull M message) { + send(topic, message, null); + } + + public void send(@NonNull T topic, @NonNull M message, S sender) { + sendSync(topic, message, sender); + } + + public void sendSync(@NonNull final T topic, @NonNull final M message, final S sender) { + log.debug("EventBus: sendSync: BEGIN: topic={}, sender={}, message={}", topic, sender, message); + checkTopic(topic); + checkSender(sender); + log.trace("EventBus: sendSync: CHECKED: topicsAndConsumers={}, consumerPatternMap={}", topicsAndConsumers, consumerPatternMap); + Set> topicConsumers = topicsAndConsumers.get(topic); + log.debug("EventBus: sendSync: CHECKED: topic={}, sender={}, message={}, consumers={}", topic, sender, message, topicConsumers); + if (topicConsumers!=null) { + topicConsumers.forEach(consumer -> { + log.debug("EventBus: sendSync: ....SENDING-TO-CONSUMER: topic={}, sender={}, consumer={}, message={}", topic, sender, consumer, message); + consumer.onMessage(topic, message, sender); + }); + } + final String topicString = topic.toString(); + consumerPatternMap.forEach((consumer, patternSet) -> patternSet.forEach(pattern -> { + log.debug("EventBus: sendSync: ....CHECKING PATTERN: topic={}, sender={}, consumer={}, pattern={}, message={}", topic, sender, consumer, pattern.pattern(), message); + if (pattern.matcher(topicString).matches()) { + log.debug("EventBus: sendSync: ....SENDING-TO-PATTERN-CONSUMER: topic={}, sender={}, consumer={}, pattern={}, message={}", topic, sender, consumer, pattern.pattern(), message); + consumer.onMessage(topic, message, sender); + } + })); + } + + public boolean subscribe(@NonNull T topic, @NonNull EventConsumer consumer) { + checkTopic(topic); + Set> topicConsumers = topicsAndConsumers.get(topic); + if (topicConsumers==null) { + synchronized (topicsAndConsumers) { + topicConsumers = topicsAndConsumers.computeIfAbsent(topic, k -> new HashSet<>()); + } + } + + return topicConsumers.add(consumer); + } + + public boolean unsubscribe(@NonNull T topic, @NonNull EventConsumer consumer) { + checkTopic(topic); + Set> topicConsumers = topicsAndConsumers.get(topic); + if (topicConsumers!=null) { + boolean result = topicConsumers.remove(consumer); + if (topicConsumers.isEmpty()) { + synchronized (topicsAndConsumers) { + topicConsumers = topicsAndConsumers.get(topic); + if (topicConsumers.isEmpty()) { + topicsAndConsumers.remove(topic); + } + } + } + return result; + } + return false; + } + + public boolean subscribePattern(@NonNull String patternString, @NonNull EventConsumer consumer) { + Pattern pattern = Pattern.compile(patternString); + List consumerPatterns = consumerPatternMap.get(consumer); + if (consumerPatterns==null) { + synchronized (consumerPatternMap) { + consumerPatterns = consumerPatternMap.computeIfAbsent(consumer, k -> new ArrayList<>()); + } + } + + return consumerPatterns.add(pattern); + } + + public boolean unsubscribePattern(@NonNull String patternString, @NonNull EventConsumer consumer) { + List consumerPatterns = consumerPatternMap.get(consumer); + if (consumerPatterns!=null) { + Optional item = consumerPatterns.stream().filter(pattern -> pattern.pattern().equals(patternString)).findAny(); + boolean result = false; + if (item.isPresent()) + result = consumerPatterns.remove(item.get()); + if (consumerPatterns.isEmpty()) { + synchronized (consumerPatternMap) { + consumerPatterns = consumerPatternMap.get(consumer); + if (consumerPatterns.isEmpty()) { + consumerPatternMap.remove(consumer); + } + } + } + return result; + } + return false; + } + + private void checkTopic(@NonNull T topic) { + if (allowedTopics==null || allowedTopics.isEmpty()) return; + if (!allowedTopics.contains(topic)) + throw new IllegalArgumentException("Topic not allowed in event bus: "+topic); + } + + private void checkSender(S sender) { + if (allowedSenders==null || allowedSenders.isEmpty()) return; + if (!allowedSenders.contains(sender)) + throw new IllegalArgumentException("Sender not allowed in event bus: "+sender); + } + + public interface EventConsumer { + void onMessage(T topic, M message, S sender); + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/FunctionDefinition.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/FunctionDefinition.java new file mode 100644 index 0000000..5f44f1d --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/FunctionDefinition.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.Getter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Getter +@ToString +public class FunctionDefinition implements Serializable { + private String name; + private String expression; + private List arguments; + + public FunctionDefinition setName(String name) { + this.name = name; + return this; + } + + public FunctionDefinition setExpression(String expression) { + this.expression = expression; + return this; + } + + public FunctionDefinition setArguments(List arguments) { + this.arguments = new ArrayList<>(arguments); + return this; + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/GROUPING.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/GROUPING.java new file mode 100644 index 0000000..87e125d --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/GROUPING.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +public enum GROUPING { + GLOBAL, + PER_CLOUD, + PER_REGION, + PER_ZONE, + PER_HOST, + PER_INSTANCE; + + public static List getNames() { + return Arrays.stream(values()) + .map(Enum::name) + .collect(Collectors.toList()); + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/GroupingConfiguration.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/GroupingConfiguration.java new file mode 100644 index 0000000..369086e --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/GroupingConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.*; + +import java.io.Serializable; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Baguette Client Grouping Configuration + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString(exclude = {"brokerPassword"}) +public class GroupingConfiguration implements Serializable { + @NonNull private String name; + private Properties properties; + @NonNull private Map brokerConnections; + @NonNull private Set eventTypeNames; + @NonNull private Map> rules; + @NonNull private Map> connections; + @NonNull private Set functionDefinitions; + @NonNull private Map constants; + private String brokerUsername; + private String brokerPassword; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @ToString(exclude = {"certificate", "password"}) + public static class BrokerConnectionConfig implements Serializable { + private String grouping; + private String url; + private String certificate; + private String username; + private String password; + } +} \ No newline at end of file diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/IKeystoreAndCertificateProperties.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/IKeystoreAndCertificateProperties.java new file mode 100644 index 0000000..62c9914 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/IKeystoreAndCertificateProperties.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +public interface IKeystoreAndCertificateProperties { + + enum KEY_ENTRY_GENERATE { YES, ALWAYS, NO, NEVER, IF_MISSING, IF_IP_CHANGED }; + + String getKeystoreFile(); + String getKeystoreType(); + String getKeystorePassword(); + String getTruststoreFile(); + String getTruststoreType(); + String getTruststorePassword(); + String getCertificateFile(); + + KEY_ENTRY_GENERATE getKeyEntryGenerate(); + String getKeyEntryName(); + String getKeyEntryDName(); + String getKeyEntryExtSAN(); +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/KeystoreAndCertificateProperties.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/KeystoreAndCertificateProperties.java new file mode 100644 index 0000000..8a623fa --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/KeystoreAndCertificateProperties.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +@ToString(exclude = {"truststorePassword", "keystorePassword"}) +public class KeystoreAndCertificateProperties implements IKeystoreAndCertificateProperties { + + private String keystoreFile; + private String keystoreType; + private String keystorePassword; + + private String truststoreFile; + private String truststoreType; + private String truststorePassword; + + private String certificateFile; + + private KEY_ENTRY_GENERATE keyEntryGenerate; + private String keyEntryName; + private String keyEntryDName; + private String keyEntryExtSAN; +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/KeystoreUtil.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/KeystoreUtil.java new file mode 100644 index 0000000..89abddb --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/KeystoreUtil.java @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.cryptacular.util.CertUtil; +import org.cryptacular.x509.GeneralNameType; +import org.springframework.util.FileCopyUtils; + +import java.io.*; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.*; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class KeystoreUtil { + private final static String DEFAULT_KEY_GEN_ALGORITHM = "RSA"; + private final static String DEFAULT_SIGNATURE_ALGORITHM = "SHA256WithRSA"; + private final static int DEFAULT_KEY_SIZE = 2048; + private final static int DEFAULT_CERT_START_DATE_OFFSET = -1; + private final static int DEFAULT_CERT_END_DATE_OFFSET = 3650; + + private final static String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + private final static String END_CERT = "-----END CERTIFICATE-----"; + private final static String LINE_SEPARATOR = System.getProperty("line.separator"); + + private static boolean bcProviderInitialized = false; + + private final String keystoreFile; + private final String keystoreType; + private final String keystorePassword; + private PasswordUtil passwordUtil; + + // KeystoreUtil instance methods + public static KeystoreUtil getKeystore(String file, String type, String password) { + return new KeystoreUtil(file, type, password); + } + + private KeystoreUtil(String file, String type, String password) { + this.keystoreFile = file; + this.keystoreType = type; + this.keystorePassword = password; + } + + // Creates a new keystore file if not already exists + public KeystoreUtil createIfNotExist() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + File f = new File(keystoreFile); + if (! f.exists()) { + log.debug("KeystoreUtil: Keystore file not found: {}", keystoreFile); + KeyStore keystore = KeyStore.getInstance(keystoreType); + keystore.load(null, keystorePassword.toCharArray()); + writeKeystore(keystore); + log.debug("KeystoreUtil: Keystore file created: {}", keystoreFile); + } else { + log.debug("KeystoreUtil: Keystore file exists: {}", keystoreFile); + } + return this; + } + + public boolean checkIfExist() { + File f = new File(keystoreFile); + return f.exists(); + } + + public PasswordUtil passwordUtil() { + if (this.passwordUtil==null) + this.passwordUtil = PasswordUtil.getInstance(); + return this.passwordUtil; + } + + public KeystoreUtil passwordUtil(PasswordUtil passwordUtil) { + this.passwordUtil = passwordUtil!=null ? passwordUtil : PasswordUtil.getInstance(); + return this; + } + + // Create/Replace Key pair and Certificate methods + // If keystore file does not exist it will be created + public KeystoreUtil createKeyAndCert(String entryName, String dn, String ext) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, OperatorCreationException + { + return createKeyAndCert(entryName, DEFAULT_KEY_GEN_ALGORITHM, DEFAULT_KEY_SIZE, DEFAULT_SIGNATURE_ALGORITHM, DEFAULT_CERT_START_DATE_OFFSET, DEFAULT_CERT_END_DATE_OFFSET, dn, ext); + } + + public KeystoreUtil createOrReplaceKeyAndCert(String entryName, String dn, String ext) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, OperatorCreationException + { + return this + .deleteEntry(entryName) + .createKeyAndCert(entryName, dn, ext); + } + + public KeystoreUtil createKeyAndCert(String entryName, String keyGenAlg, int keySize, String sigAlg, int startDateOffset, int endDateOffset, String dn, String extSAN) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, OperatorCreationException + { + boolean hasExt = StringUtils.isNotBlank(extSAN); + + // Read keystore from file or create it + log.trace("KeystoreUtil: Reading keystore from file: {}", keystoreFile); + KeyStore keystore; + try { + keystore = readKeystore(); + log.debug("KeystoreUtil: Keystore loaded from file: {}", keystoreFile); + } catch (FileNotFoundException e) { + log.info("KeystoreUtil: Keystore file will be created: {}", keystoreFile); + //keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore = KeyStore.getInstance(keystoreType); + keystore.load(null, keystorePassword.toCharArray()); + } + + // Generate new key pair + log.trace("KeystoreUtil: Creating entry: {}", entryName); + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(keyGenAlg); + keyPairGen.initialize(keySize); + KeyPair keyPair = keyPairGen.generateKeyPair(); + + // Compute validity period of certificate (will be generated next) + long now = System.currentTimeMillis(); + Date dtNow = new Date(now); + Calendar calendar = Calendar.getInstance(); + + calendar.setTime(dtNow); + calendar.add(Calendar.DATE, startDateOffset); + Date validFrom = calendar.getTime(); + + calendar.setTime(dtNow); + calendar.add(Calendar.DATE, endDateOffset); + Date validTo = calendar.getTime(); + + // Register Bouncy-Castle provider (if not already) + if (!bcProviderInitialized) { + bcProviderInitialized = true; + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + // Generate new certificate for key pair + X500Name subjectName = new X500Name(dn); + X500Name issuerName = subjectName; + BigInteger serialNumber = new BigInteger(Long.toString(now)); + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + issuerName, serialNumber, validFrom, validTo, subjectName, + SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()) + ); + + // Add certificate extensions + JcaX509ExtensionUtils jcaExtUtils = new JcaX509ExtensionUtils(); + //X509Certificate caCert = null; + //certBuilder.addExtension(Extension.authorityKeyIdentifier, false, + // jcaExtUtils.createAuthorityKeyIdentifier(caCert)); + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, + jcaExtUtils.createSubjectKeyIdentifier(keyPair.getPublic())); + if (hasExt) { + extSAN = extSAN.replaceAll("[ \t\r\n]]+",""); + String[] names = extSAN.split(","); + List altNames = new ArrayList<>(); + for (String name : names) { + if (StringUtils.startsWithIgnoreCase(name, "dns:")) { + name = name.substring("dns:".length()); + if (StringUtils.isNotBlank(name)) + altNames.add(new GeneralName(GeneralName.dNSName, name)); + } else + if (StringUtils.startsWithIgnoreCase(name, "ip:")) { + name = name.substring("ip:".length()); + if (StringUtils.isNotBlank(name)) + altNames.add(new GeneralName(GeneralName.iPAddress, name)); + } else + log.warn("KeystoreUtil: Ignoring element of Subject Alt. Names: {}", name); + } + GeneralNames subjectAltName = new GeneralNames(altNames.toArray(new GeneralName[0])); + certBuilder.addExtension(Extension.subjectAlternativeName, false, subjectAltName); + } + + // Self-Sign and get certificate + JcaContentSignerBuilder builder = new JcaContentSignerBuilder(sigAlg); + ContentSigner signer = builder.build(keyPair.getPrivate()); + + byte[] certBytes = certBuilder.build(signer).getEncoded(); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate)certificateFactory.generateCertificate(new ByteArrayInputStream(certBytes)); + + // Add/Replace key pair and certificate chain to keystore + PrivateKey newKey = keyPair.getPrivate(); + Certificate[] chain = new Certificate[] { certificate }; + String entryPassword = keystorePassword; + keystore.setKeyEntry(entryName, newKey, entryPassword.toCharArray(), chain); + log.debug("KeystoreUtil: Entry created: {}", entryName); + + // Store keystore back to file + log.trace("KeystoreUtil: Writing keystore to file: {}", keystoreFile); + writeKeystore(keystore); + log.debug("KeystoreUtil: Keystore stored to file: {}", keystoreFile); + + return this; + } + + public KeystoreUtil createOrReplaceKeyAndCert(String entryName, String keyGenAlg, int keySize, String sigAlg, int startDateOffset, int endDateOffset, String dn, String ext) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, OperatorCreationException + { + return this + .deleteEntry(entryName) + .createKeyAndCert(entryName, keyGenAlg, keySize, sigAlg, startDateOffset, endDateOffset, dn, ext); + } + + public KeystoreUtil createKeyAndCertWithSAN(String entryName, String dn) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, OperatorCreationException + { + String sanExt = String.format("dns:localhost,ip:127.0.0.1,ip:%s,ip:%s", + NetUtil.getDefaultIpAddress(), NetUtil.getPublicIpAddress()); + return createKeyAndCert(entryName, dn, sanExt); + } + + public KeystoreUtil createOrReplaceKeyAndCertWithSAN(String entryName, String dn) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, OperatorCreationException + { + return this + .deleteEntry(entryName) + .createKeyAndCertWithSAN(entryName, dn); + } + + // Delete key pair and/or certificate from keystore + public KeystoreUtil deleteEntry(String entryName) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException + { + try { + log.trace("KeystoreUtil: Deleting entry from keystore: alias={}, file={}", entryName, keystoreFile); + KeyStore keystore = readKeystore(); + keystore.deleteEntry(entryName); + writeKeystore(keystore); + log.debug("KeystoreUtil: Entry deleted from keystore: alias={}, file={}", entryName, keystoreFile); + } catch (FileNotFoundException e) { + log.debug("KeystoreUtil: Keystore file not exists: {}", keystoreFile); + } + return this; + } + + // Query if alias exists in keystore + public boolean containsEntry(String entryName) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException + { + KeyStore keystore = readKeystore(keystoreFile, keystoreType, keystorePassword); + return keystore.containsAlias(entryName); + } + + // Certificate import/export methods + public KeystoreUtil importAndReplaceCertFromFile(String entryName, String certFile) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException + { + return this + .deleteEntry(entryName) + .importCertFromFile(entryName, certFile); + } + + public KeystoreUtil importCertFromFile(String entryName, String certFile) + throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException + { + log.debug("KeystoreUtil: Reading certificate from file: {}", certFile); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate cert = cf.generateCertificate(Files.newInputStream(Paths.get(certFile))); + log.trace("KeystoreUtil: Certificate: {}", cert); + + log.trace("KeystoreUtil: Importing certificate to keystore file: alias={}, file={}", entryName, keystoreFile); + KeyStore keystore = readKeystore(keystoreFile, keystoreType, keystorePassword); + keystore.setCertificateEntry(entryName, cert); + writeKeystore(keystore, keystoreFile, keystoreType, keystorePassword); + log.debug("KeystoreUtil: Certificate imported into keystore: alias={}, file={}", entryName, keystoreFile); + + return this; + } + + public KeystoreUtil exportCertToFile(String entryName, String certFile) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException + { + log.debug("KeystoreUtil: Reading certificate from keystore: alias={}, keystore={}", entryName, keystoreFile); + String certPem = getEntryCertificateAsPEM(entryName); + log.trace("KeystoreUtil: Certificate (PEM):\n{}", certPem); + + log.trace("KeystoreUtil: Storing certificate to file: {}", certFile); + try (PrintStream ps = new PrintStream(Files.newOutputStream(Paths.get(certFile)))) { + ps.print(certPem); + ps.flush(); + } + log.debug("KeystoreUtil: Certificate stored in file: {}", certFile); + return this; + } + + // Certificate retrieval methods + public X509Certificate getEntryCertificate(String entryName) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException + { + log.trace("KeystoreUtil.getEntryCertificate(): keystore: file={}, type={}, password={}", + keystoreFile, keystoreType, passwordUtil().encodePassword(keystorePassword)); + KeyStore keystore = readKeystore(keystoreFile, keystoreType, keystorePassword); + log.trace("KeystoreUtil.getEntryCertificate(): keystore: {}", keystore); + log.trace("KeystoreUtil.getEntryCertificate(): entry-name: {}", entryName); + return (X509Certificate) keystore.getCertificate(entryName); + } + + public String getEntryCertificateAsPEM(String entryName) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException + { + X509Certificate cert = getEntryCertificate(entryName); + log.trace("KeystoreUtil.getEntryCertificatePEM(): X509 certificate:\n{}", cert); + String certPem = exportCertificateAsPEM(cert); + log.trace("KeystoreUtil.getEntryCertificatePEM(): X509 certificate (PEM):\n{}", certPem); + return certPem; + } + + public byte[] getEntryCertificateAsDER(String entryName) throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + X509Certificate cert = getEntryCertificate(entryName); + log.trace("KeystoreUtil.getEntryCertificatePEM(): X509 certificate:\n{}", cert); + byte[] certBytes = cert.getEncoded(); + log.trace("KeystoreUtil.getEntryCertificatePEM(): X509 certificate (DER):\n{}", certBytes); + return certBytes; + } + + public static String exportCertificateAsPEM(X509Certificate cert) throws CertificateEncodingException { + log.trace("KeystoreUtil.exportCertificateAsPEM(): X509 certificate:\n{}", cert); + byte[] certBytes = cert.getEncoded(); + Base64.Encoder encoder = Base64.getMimeEncoder(64, LINE_SEPARATOR.getBytes()); + String certEncoded = new String(encoder.encode(certBytes)); + String certPem = + BEGIN_CERT + LINE_SEPARATOR + certEncoded + LINE_SEPARATOR + END_CERT; + log.trace("KeystoreUtil.exportCertificateAsPEM(): X509 certificate (PEM):\n{}", certPem); + return certPem; + } + + public static byte[] exportCertificateAsDER(X509Certificate cert) throws CertificateEncodingException { + log.trace("KeystoreUtil.exportCertificateAsDER(): X509 certificate:\n{}", cert); + byte[] certBytes = cert.getEncoded(); + log.trace("KeystoreUtil.exportCertificateAsDER(): X509 certificate (DER):\n{}", certBytes); + return certBytes; + } + + // Certificate names methods + public List getEntryNames(String entryName, boolean onlyIp) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException + { + X509Certificate cert = getEntryCertificate(entryName); + if (cert==null) { + log.debug("KeystoreUtil.getEntryNames(): No certificate found for {}", entryName); + return Collections.emptyList(); + } + + List names = onlyIp + ? CertUtil.subjectNames(cert, GeneralNameType.IPAddress) + : CertUtil.subjectNames(cert); + return names.stream() + .map(sanName -> { + try { + return sanName.startsWith("#") ? + InetAddress.getByAddress(parseHexToBinary(sanName.substring(1))).getHostAddress() + : sanName; + } catch (Exception ex) { + log.warn("KeystoreUtil: getEntryNames: entry={} caused {}", sanName, ex.toString()); + log.debug("KeystoreUtil: getEntryNames: entry={} caused:\n", sanName, ex); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private byte[] parseHexToBinary(String hexValue) { + byte[] ip = new byte[hexValue.length()/2]; + for(int i = 0, j = 0; i < hexValue.length(); i = i + 2) { + ip[j++] = (byte)Integer.parseInt(hexValue.substring(i, i+2), 16); + } + if (log.isTraceEnabled()) log.trace("KeystoreUtil.parseHexBinary(): hex={}, ip={}", hexValue, Arrays.toString(ip)); + return ip; + } + + // Certificate listing methods + public List getCertificateAliases() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + KeyStore ks = KeystoreUtil.readKeystore(keystoreFile, keystoreType, keystorePassword); + return getCertificateAliases(ks); + } + + public static List getCertificateAliases(KeyStore ks) throws KeyStoreException { + List certAliases = new ArrayList<>(); + Enumeration en = ks.aliases(); + while (en.hasMoreElements()) { + String alias = en.nextElement(); + log.trace("KeystoreUtil.getCertificateAliases(): Checking alias: {}", alias); + if (ks.isCertificateEntry(alias)) { + certAliases.add(alias); + log.trace("KeystoreUtil.getCertificateAliases(): Alias added in results: {}", alias); + } + } + log.trace("KeystoreUtil.getCertificateAliases(): Certificate aliases: {}", certAliases); + return certAliases; + } + + // Keystore read/write methods + public KeyStore readKeystore() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + return KeystoreUtil.readKeystore(keystoreFile, keystoreType, keystorePassword); + } + + public KeystoreUtil writeKeystore(KeyStore keystore) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + KeystoreUtil.writeKeystore(keystore, keystoreFile, keystoreType, keystorePassword); + return this; + } + + public String readFileAsBase64() throws IOException { + byte[] encoded = Base64.getEncoder().encode(FileCopyUtils.copyToByteArray(new File(keystoreFile))); + return new String(encoded, StandardCharsets.US_ASCII); + } + + public KeystoreUtil writeBase64ToFile(String base64) throws IOException { + byte[] bytes = Base64.getDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII)); + FileCopyUtils.copy(bytes, new File(keystoreFile)); + return this; + } + + public static KeyStore readKeystore(String file, String type, String password) + throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException + { + KeyStore keystore = KeyStore.getInstance(type); + try (FileInputStream fis = new FileInputStream(file)) { + keystore.load(fis, password.toCharArray()); + } + return keystore; + } + + public static void writeKeystore(KeyStore keystore, String file, String type, String password) + throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException + { + try (FileOutputStream fos = new FileOutputStream(file)) { + keystore.store(fos, password.toCharArray()); + } + } + + // Keystore, Trust store and Certificate initialization based on a properties source + public static void initializeKeystoresAndCertificate(IKeystoreAndCertificateProperties properties, PasswordUtil passwordUtil) + throws CertificateException, KeyStoreException, NoSuchAlgorithmException, IOException, OperatorCreationException + { + if (passwordUtil==null) + passwordUtil = PasswordUtil.getInstance(); + log.debug("KeystoreUtil.initializeKeystoresAndCertificate(): Initializing keystores and certificate..."); + log.debug("KeystoreUtil.initializeKeystoresAndCertificate(): Key pair and Certificate settings:"); + log.debug(" Keystore file: {}", properties.getKeystoreFile()); + log.debug(" Keystore type: {}", properties.getKeystoreType()); + log.debug(" Keystore password: {}", passwordUtil.encodePassword(properties.getKeystorePassword())); + log.debug(" Trust store file: {}", properties.getTruststoreFile()); + log.debug(" Trust store type: {}", properties.getTruststoreType()); + log.debug(" Trust store password: {}", passwordUtil.encodePassword(properties.getTruststorePassword())); + log.debug(" Certificate file: {}", properties.getCertificateFile()); + log.debug(" Entry name: {}", properties.getKeyEntryName()); + log.debug(" Entry DName: {}", properties.getKeyEntryDName()); + log.debug(" Entry SAN: {}", properties.getKeyEntryExtSAN()); + log.debug(" Entry Gen.: {}", properties.getKeyEntryGenerate()); + + IKeystoreAndCertificateProperties.KEY_ENTRY_GENERATE keyGen = properties.getKeyEntryGenerate(); + boolean gen = (keyGen==IKeystoreAndCertificateProperties.KEY_ENTRY_GENERATE.YES || keyGen==IKeystoreAndCertificateProperties.KEY_ENTRY_GENERATE.ALWAYS); + + // Check if key entry is missing + if (keyGen==IKeystoreAndCertificateProperties.KEY_ENTRY_GENERATE.IF_MISSING) { + // Check if keystore and truststore files exist (and create if they don't) + KeystoreUtil + .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) + .createIfNotExist(); + KeystoreUtil + .getKeystore(properties.getTruststoreFile(), properties.getTruststoreType(), properties.getTruststorePassword()) + .passwordUtil(passwordUtil) + .createIfNotExist(); + + // Check if entry with given 'alias' already exists in keystore + boolean containsEntry = KeystoreUtil + .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) + .containsEntry(properties.getKeyEntryName()); + if (containsEntry) { + log.debug(" Keystore already contains entry: {}", properties.getKeyEntryName()); + } else { + log.debug(" Keystore does not contain entry: {}", properties.getKeyEntryName()); + gen = true; + } + } + + // Check if IP address is in subject CN or SAN list + if (keyGen==IKeystoreAndCertificateProperties.KEY_ENTRY_GENERATE.IF_IP_CHANGED) { + // Check if keystore and truststore files exist (and create if they don't) + KeystoreUtil + .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) + .createIfNotExist(); + KeystoreUtil + .getKeystore(properties.getTruststoreFile(), properties.getTruststoreType(), properties.getTruststorePassword()) + .passwordUtil(passwordUtil) + .createIfNotExist(); + + // get subject CN and SAN list (IP's only) + List addrList = KeystoreUtil + .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) + .getEntryNames(properties.getKeyEntryName(), true); + log.debug(" Entry addresses: {}", addrList); + + // get current Default and Public IP addresses + String defaultIp = NetUtil.getDefaultIpAddress(); + String publicIp = NetUtil.getPublicIpAddress(); + + // check if Default and Public IP addresses are contained in 'addrList' + boolean defaultFound = addrList.stream().anyMatch(s -> s.equals(defaultIp)); + boolean publicFound = addrList.stream().anyMatch(s -> s.equals(publicIp)); + gen = !defaultFound || !publicFound; + log.debug(" Address has changed: {} (default-ip-found={}, public-ip-found={})", + gen, defaultFound, publicFound); + } + + // Generate new key pair and certificate, and update keystore and trust store + if (gen) { + log.debug(" Generating new Key pair and Certificate for: {}", properties.getKeyEntryName()); + + KeystoreUtil ksUtil = KeystoreUtil + .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) + .createIfNotExist(); + if (StringUtils.isBlank(properties.getKeyEntryExtSAN())) { + log.debug(" Create/Replace entry (with SAN auto-generate): {}", properties.getKeyEntryName()); + ksUtil.createOrReplaceKeyAndCertWithSAN(properties.getKeyEntryName(), properties.getKeyEntryDName()); + } else { + log.debug(" Create/Replace entry and SAN: entry={}, san={}", + properties.getKeyEntryName(), properties.getKeyEntryExtSAN()); + String extSAN = properties.getKeyEntryExtSAN().trim(); + ksUtil.createOrReplaceKeyAndCert(properties.getKeyEntryName(), properties.getKeyEntryDName(), extSAN); + } + log.debug(" Exporting certificate to: {}", properties.getCertificateFile()); + ksUtil.exportCertToFile(properties.getKeyEntryName(), properties.getCertificateFile()); + + KeystoreUtil tsUtil = KeystoreUtil + .getKeystore(properties.getTruststoreFile(), properties.getTruststoreType(), properties.getTruststorePassword()) + .passwordUtil(passwordUtil) + .createIfNotExist(); + log.debug(" Importing certificate to trust store: {}", properties.getTruststoreFile()); + tsUtil.importAndReplaceCertFromFile(properties.getKeyEntryName(), properties.getCertificateFile()); + + log.debug(" Key pair and Certificate generation completed"); + } else { + log.debug(" Key pair and Certificate will not be re-generated"); + } + + // Log PEM certificate + if (log.isDebugEnabled()) { + String certPemStr = KeystoreUtil + .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) + .getEntryCertificateAsPEM(properties.getKeyEntryName()); + log.debug(" Certificate (PEM):\n{}", certPemStr); + } + log.debug("KeystoreUtil.initializeKeystoresAndCertificate(): Initializing keystores and certificate... done"); + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/NetUtil.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/NetUtil.java new file mode 100644 index 0000000..6142230 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/NetUtil.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.net.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Network Utility + */ +@Slf4j +public class NetUtil { + + private final static String[] ADDRESS_FILTERS; + + private final static String DATAGRAM_ADDRESS; + + private final static String[][] PUBLIC_ADDRESS_DISCOVERY_SERVICES; + + static { + // Configure Address Filters + String filtersStr = System.getenv("NET_UTIL_ADDRESS_FILTERS"); + List filtersList = new ArrayList<>(); + if (StringUtils.isNotBlank(filtersStr)) { + filtersList = Arrays.stream(filtersStr.split("[;, \t]+")).map(String::trim).filter(s->!s.isEmpty()).collect(Collectors.toList()); + } else { + filtersList = Arrays.asList( + "127.", + /*"192.168.", "10.", "172.16.", "172.31.", "169.254.",*/ + "224.", "239.", "255.255.255.255" + ); + } + ADDRESS_FILTERS = filtersList.toArray(new String[0]); + + // Configure Datagram address + String datagramAddress = System.getenv("NET_UTIL_DATAGRAM_ADDRESS"); + DATAGRAM_ADDRESS = StringUtils.isNotBlank(datagramAddress) ? datagramAddress.trim() : "8.8.8.8"; + + // Configure Address discovery services + String servicesStr = System.getenv("NET_UTIL_ADDRESS_DISCOVERY_SERVICES"); + List servicesList = new ArrayList<>(); + if (StringUtils.isNotBlank(servicesStr)) { + if (!"-".equals(servicesStr)) { + Arrays.stream(servicesStr.split("[;, \t]+")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(s -> s.split("[:=]", 2)) + .filter(a -> a.length == 2) + .peek(a->{ a[0]=a[0].trim(); a[1]=a[1].trim(); }) + .filter(a->!a[0].isEmpty() && !a[1].isEmpty()) + .forEach(servicesList::add); + } + } else { + servicesList.add(Arrays.asList("AWS", "http://checkip.amazonaws.com").toArray(new String[0])); + servicesList.add(Arrays.asList("Ipify", "https://api.ipify.org/?format=text").toArray(new String[0])); + servicesList.add(Arrays.asList("WhatIsMyIpAddress", "http://bot.whatismyipaddress.com/").toArray(new String[0])); + } + PUBLIC_ADDRESS_DISCOVERY_SERVICES = servicesList.toArray(new String[0][]); + } + + // ------------------------------------------------------------------------ + + public static void main(String[] args) throws IOException { + for (String arg : args) { + if ("-nolog".equalsIgnoreCase(arg)) { + loggingOff = true; + } else + if ("-log-all".equalsIgnoreCase(arg)) { + logAll = true; + } else + if ("public".equalsIgnoreCase(arg)) { + printAddress(getPublicIpAddress()); + } else + if ("default".equalsIgnoreCase(arg)) { + printAddress(getDefaultIpAddress()); + } else + if ("addresses".equalsIgnoreCase(arg)) { + for (InetAddress addr : getIpAddresses()) { + printAddress(addr.getHostAddress()); + } + } else + { + for (String[] service : PUBLIC_ADDRESS_DISCOVERY_SERVICES) { + if (service[0].equalsIgnoreCase(arg)) { + printAddress(queryService(service[1])); + } + } + } + } + } + + protected static void printAddress(String addr) { + if (logAll) log_info("{}", addr); + else System.out.println(addr); + } + + // ------------------------------------------------------------------------ + + protected static boolean loggingOff = false; + protected static boolean logAll = false; + + protected static void log_trace(String s, Object...o) { if (loggingOff) return; log.trace(s, o); } + protected static void log_debug(String s, Object...o) { if (loggingOff) return; log.debug(s, o); } + protected static void log_info(String s, Object...o) { if (loggingOff) return; log.info(s, o); } + protected static void log_warn(String s, Object...o) { if (loggingOff) return; log.warn(s, o); } + + // ------------------------------------------------------------------------ + + protected static boolean cacheAddresses = true; + + public static boolean isCacheAddresses() { return cacheAddresses; } + public static void setCacheAddresses(boolean b) { cacheAddresses = b; } + + public static void clearCaches() { + ipAddresses = null; + publicIpAddress = null; + defaultIpAddress = null; + } + + // ------------------------------------------------------------------------ + + private static List ipAddresses = null; + + public static List getIpAddresses() throws SocketException { + if (cacheAddresses && ipAddresses!=null) { + log_debug("NetUtil.getIpAddresses(): Returning cached IP addresses: {}", ipAddresses); + return ipAddresses; + } + + List list = new ArrayList<>(); + Enumeration en = NetworkInterface.getNetworkInterfaces(); + while (en.hasMoreElements()) { + NetworkInterface ni = en.nextElement(); + for (InterfaceAddress ia : ni.getInterfaceAddresses()) { + InetAddress inet = ia.getAddress(); + if (inet instanceof java.net.Inet4Address) { + String addr = inet.getHostAddress(); + if (!inet.isLoopbackAddress() && !inet.isMulticastAddress() && inet.isSiteLocalAddress()) { + boolean ok = Arrays.stream(ADDRESS_FILTERS) + .noneMatch(addr::startsWith); + if (ok) { + log_debug("{}", addr); + list.add(inet); + } + } + } + } + } + if (cacheAddresses) ipAddresses = Collections.unmodifiableList(list); + return list; + } + + protected static InetAddress _getIpAddress() { + try { + List list = getIpAddresses(); + if (list.size() == 0) { + log_debug("NetUtil.getIpAddress(): Returning 'null' because getIpAddresses() returned an empty list"); + return null; + } + return list.get(0); + } catch (SocketException se) { + log_debug("NetUtil.getIpAddress(): Returning 'null' due to exception: ", se); + return null; + } + } + + public static String getIpAddress() { + return _getIpAddress().getHostAddress(); + } + + public static String getHostname() { + return _getIpAddress().getHostName(); + } + + public static String getCanonicalHostName() { + return _getIpAddress().getCanonicalHostName(); + } + + // ------------------------------------------------------------------------ + + private static String publicIpAddress = null; + + public static String getPublicIpAddress() { + if (cacheAddresses && publicIpAddress!=null) { + log_debug("NetUtil.getPublicIpAddress(): Returning cached Public IP address: {}", publicIpAddress); + return publicIpAddress; + } + + for (String[] service : PUBLIC_ADDRESS_DISCOVERY_SERVICES) { + log_debug("NetUtil.getPublicIpAddress(): Contacting service {}", service[0]); + String ip = getIpAddressUsingService(service[1]); + if (StringUtils.isNotBlank(ip)) { + String addr = ip.trim(); + if (cacheAddresses) publicIpAddress = addr; + log_debug("NetUtil.getPublicIpAddress(): Public IP address: {}", addr); + return addr; + } + } + if (cacheAddresses) publicIpAddress = ""; + + log_warn("NetUtil.getPublicIpAddress(): No Public IP address or connectivity problems exist"); + return null; + } + + private static String getIpAddressUsingService(String url) { + try { + log_debug("NetUtil.getIpAddressUsingService(): Service URL: {}", url); + String response = queryService(url); + log_debug("NetUtil.getIpAddressUsingService(): Service response: {}", response); + if (StringUtils.isNotBlank(response)) { + return response; + } + } catch (Exception ex) { + log_warn("NetUtil.getIpAddressUsingService(): Contacting service FAILED: url={}, EXCEPTION={}", url, ex.toString()); + log_trace("NetUtil.getIpAddressUsingService(): Exception stack trace: ", ex); + } + + log_debug("NetUtil.getIpAddressUsingService(): Response is null or blank"); + return null; + } + + private static String queryService(String url) throws MalformedURLException, IOException { + try (Scanner s = new Scanner(new URL(url).openStream(), "UTF-8").useDelimiter("\\A")) { + return s.next().trim(); + } + } + + // ------------------------------------------------------------------------ + + private static String defaultIpAddress = null; + + public static String getDefaultIpAddress() { + if (cacheAddresses && defaultIpAddress!=null) { + log_debug("NetUtil.getDefaultIpAddress(): Returning cached Default IP address: {}", defaultIpAddress); + return defaultIpAddress; + } + + try { + log_debug("NetUtil.getDefaultIpAddress(): Datagram address: {}", DATAGRAM_ADDRESS); + String addr = getIpAddressWithDatagram(DATAGRAM_ADDRESS); + if (cacheAddresses) defaultIpAddress = addr; + log_debug("NetUtil.getDefaultIpAddress(): Response: {}", addr); + if (StringUtils.isNotBlank(defaultIpAddress)) return addr; + } catch (Exception ex) { + log_warn("NetUtil.getDefaultIpAddress(): Datagram method failed: outgoing-ip-address={}, exception=", DATAGRAM_ADDRESS, ex); + if (cacheAddresses) defaultIpAddress = ""; + } + + log_warn("NetUtil.getDefaultIpAddress(): Address is null or blank"); + return null; + } + + public static String getIpAddressWithDatagram(String address) throws SocketException, UnknownHostException { + try(final DatagramSocket socket = new DatagramSocket()) { + socket.connect(InetAddress.getByName(address), 10002); + return socket.getLocalAddress().getHostAddress(); + } + } + + // ------------------------------------------------------------------------ + + public static boolean isLocalAddress(String addr) throws UnknownHostException { + return isLocalAddress(InetAddress.getByName(addr)); + } + + // Source: https://stackoverflow.com/questions/2406341/how-to-check-if-an-ip-address-is-the-local-host-on-a-multi-homed-system + public static boolean isLocalAddress(InetAddress addr) { + // Check if the address is a valid special local or loop back + if (addr.isAnyLocalAddress() || addr.isLoopbackAddress()) { + return true; + } + + // Check if the address is defined on any interface + try { + return NetworkInterface.getByInetAddress(addr) != null; + } catch (SocketException e) { + return false; + } + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/NetUtilPostProcessor.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/NetUtilPostProcessor.java new file mode 100644 index 0000000..7fe780d --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/NetUtilPostProcessor.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.stereotype.Component; + +@Component +public class NetUtilPostProcessor implements EnvironmentPostProcessor { + private static final DeferredLog log = new DeferredLog(); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + PropertySource ps = new NetUtilPropertySource(); + environment.getPropertySources().addFirst(ps); + log.info("NetUtilPostProcessor: NetUtilPropertySource registered (deferred log)"); + + application.addInitializers(ctx -> log.replayTo(NetUtilPostProcessor.class)); + } + + @Getter @Setter + public static class NetUtilPropertySource extends PropertySource { + private String defaultDefaultIp = "127.0.0.1"; + private String defaultPublicIp = "127.0.0.1"; + + public NetUtilPropertySource() { + super("ems-net-util-property-source"); + } + + public NetUtilPropertySource(String name) { + super(name); + } + + @Override + public String getProperty(String s) { + String address = null; + if ("DEFAULT_IP".equals(s)) { + address = NetUtil.getDefaultIpAddress(); + if (address==null) address = defaultDefaultIp; + } + if ("PUBLIC_IP".equals(s)) { + address = NetUtil.getPublicIpAddress(); + if (address==null) address = defaultPublicIp; + } + return address; + } + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/PasswordUtil.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/PasswordUtil.java new file mode 100644 index 0000000..cd95a0a --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/PasswordUtil.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import gr.iccs.imu.ems.util.password.AsterisksPasswordEncoder; +import gr.iccs.imu.ems.util.password.PasswordEncoder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; + +import java.lang.reflect.InvocationTargetException; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PasswordUtil implements InitializingBean { + private final static Supplier passwordEncoderSupplier = AsterisksPasswordEncoder::new; + private final static AtomicReference defaultPasswordEncoder = new AtomicReference<>(); + + private final static Object LOCK = new Object(); + private static volatile PasswordUtil instance; + + private final PasswordUtilProperties properties; + private PasswordEncoder passwordEncoder; + + public static PasswordUtil getInstance() { + if (instance==null) { + synchronized (LOCK) { + if (instance==null) { + instance = new PasswordUtil(new PasswordUtilProperties()); + instance.afterPropertiesSet(); + } + } + } + return instance; + } + + @Override + public void afterPropertiesSet() { + String passwordEncoderClassName = properties.getPasswordEncoderClass(); + log.debug("PasswordUtil: password-encoder-class: {}", passwordEncoderClassName); + this.setPasswordEncoder(StringUtils.trim(passwordEncoderClassName)); + if (passwordEncoder!=null) + if (defaultPasswordEncoder.compareAndSet(null, passwordEncoder)) + log.info("PasswordUtil: Initialized default Password Encoder: {}", defaultPasswordEncoder.get().getClass().getName()); + } + + public String encodePassword(String password) { + return getPasswordEncoder().encode(password); + } + + public PasswordEncoder getPasswordEncoder() { + return passwordEncoder!=null + ? passwordEncoder : (passwordEncoder=createPasswordEncoder(null)); + } + + public void setPasswordEncoder(PasswordEncoder pe) { + passwordEncoder = pe; + log.debug("PasswordUtil.setPasswordEncoder(): PasswordEncoder set to: {}", passwordEncoder.getClass().getName()); + } + + public void setPasswordEncoder(String passwordEncoderClassName) { + setPasswordEncoder(createPasswordEncoder(passwordEncoderClassName)); + } + + public static PasswordEncoder createPasswordEncoder(String passwordEncoderClassName) { + if (StringUtils.isBlank(passwordEncoderClassName)) { + log.debug("Password encoder class name is empty. Default instance of PasswordEncoder will be created"); + return passwordEncoderSupplier.get(); + } + + try { + Class passwordEncoderClass = Class.forName(passwordEncoderClassName); + return (PasswordEncoder) passwordEncoderClass.getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) { + log.warn("Could not instantiate PasswordEncoder instance of {}. Default instance of PasswordEncoder will be created", passwordEncoderClassName); + return passwordEncoderSupplier.get(); + } + } + + public static PasswordEncoder getDefaultPasswordEncoder() { + return Optional.ofNullable(defaultPasswordEncoder.get()) + .orElse(passwordEncoderSupplier.get()); + } + + @Slf4j + @Data + @Configuration + @ConfigurationProperties(prefix = EmsConstant.EMS_PROPERTIES_PREFIX) + public static class PasswordUtilProperties implements InitializingBean { + private String passwordEncoderClass; + + @Override + public void afterPropertiesSet() throws Exception { + log.debug("PasswordUtilProperties: {}", this); + } + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/Plugin.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/Plugin.java new file mode 100644 index 0000000..3cae229 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/Plugin.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +public interface Plugin { + default void start() {} + default void stop() {} +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/SerializationUtil.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/SerializationUtil.java new file mode 100644 index 0000000..1a6ce5f --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/SerializationUtil.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.util.Base64; + +@Slf4j +public class SerializationUtil { + /** + * Write an object to Base64 string. + */ + public static String serializeToString(Object o) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(o); + oos.close(); + return Base64.getEncoder().encodeToString(baos.toByteArray()); + } + + /** + * Read the object from Base64 string. + */ + public static Object deserializeFromString(String s) throws IOException, ClassNotFoundException { + byte[] data = Base64.getDecoder().decode(s); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); + Object o = ois.readObject(); + ois.close(); + return o; + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/StrUtil.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/StrUtil.java new file mode 100644 index 0000000..51d1f82 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/StrUtil.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Slf4j +public class StrUtil { + + // ------------------------------------------------------------------------ + // Key variations methods + // ------------------------------------------------------------------------ + + private final static Pattern pUppercase = Pattern.compile("(?=\\p{Lu})"); + private final static Pattern pDelims = Pattern.compile("[\\.\\-_ ]"); + + public static Set getVariations(@NonNull String s) { + return getVariations(s, false, null); + } + + public static Set getVariations(@NonNull String s, boolean alsoSplitOnUppercase) { + return getVariations(s, alsoSplitOnUppercase, null); + } + + public static Set getVariations(@NonNull String s, String delims) { + return getVariations(s, false, delims); + } + + public static Set getVariations(@NonNull String str, boolean alsoSplitOnUppercase, String delims) { + log.trace("StrUtil: getVariations: BEGIN: str={}, also-split-on-uppercase={}, delimiters={}", str, alsoSplitOnUppercase, delims); + // Split input string into separate words + List words = _splitToWords(str, alsoSplitOnUppercase, delims); + + // Create variations + Set variations = new LinkedHashSet<>(Arrays.asList( + // All letters plain separated with underscores + String.join("_", words), + // All letters capital separated with underscores + words.stream().map(String::toUpperCase).collect(Collectors.joining("_")), + // Title case + words.stream().map(StringUtils::capitalize).collect(Collectors.joining()), + // Camel case + StringUtils.uncapitalize(words.stream().map(StringUtils::capitalize).collect(Collectors.joining())), + // All letters lower case separated with periods + String.join(".", words), + // All letters lower case separated with dashes + String.join("-", words) + )); + log.trace("StrUtil: getVariations: END: variations: {}", variations); + return variations; + } + + public static String getNormalizedForm(@NonNull String str, boolean alsoSplitOnUppercase) { + return getNormalizedForm(str, alsoSplitOnUppercase, null); + } + + public static String getNormalizedForm(@NonNull String str, boolean alsoSplitOnUppercase, String delims) { + log.trace("StrUtil: getNormalizedForm: BEGIN: str={}, also-split-on-uppercase={}, delimiters={}", str, alsoSplitOnUppercase, delims); + // Split input string into separate words + List words = _splitToWords(str, alsoSplitOnUppercase, delims); + + // Create normalized form + String normalizedForm = words.stream().map(String::toUpperCase).collect(Collectors.joining("_")); + log.trace("StrUtil: getNormalizedForm: END: normalized form: {}", normalizedForm); + return normalizedForm; + } + + private static List _splitToWords(String str, boolean alsoSplitOnUppercase, String delims) { + log.trace("StrUtil: _splitToWords: BEGIN: str={}, also-split-on-uppercase={}, delimiters={}", str, alsoSplitOnUppercase, delims); + Pattern _delims = StringUtils.isNotBlank(delims) + ? Pattern.compile(delims) + : pDelims; + log.trace("StrUtil: _splitToWords: Effective delimiters={}", _delims); + + List words = + (alsoSplitOnUppercase + ? pUppercase.splitAsStream(str) + .filter(StringUtils::isNotBlank) + .flatMap(_delims::splitAsStream) + : _delims.splitAsStream(str) + ) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .map(String::toLowerCase) + .collect(Collectors.toList()); + log.trace("StrUtil: _splitToWords: END: words: {}", words); + return words; + } + + // ------------------------------------------------------------------------ + // Get Map values using key variations + // ------------------------------------------------------------------------ + + public static String getWithVariations(@NonNull Map configuration, @NonNull String key) { + return getWithVariations(configuration, key, null); + } + + public static String getWithVariations(@NonNull Map configuration, @NonNull String key, String defaultValue) { + log.trace("StrUtil: getWithVariations: BEGIN: key={}, default={}, map={}", key, defaultValue, configuration); + + // Create key variations + Set variations = StrUtil.getVariations(key, true); + variations.add(key); + log.trace("StrUtil: getWithVariations: variations={}", variations); + + // Search for value + for (String k : variations) { + if (configuration.containsKey(k)) { + log.trace("StrUtil: getWithVariations: Variation matched: name={}, value={}", k, configuration.get(k)); + return configuration.get(k); + } + } + log.trace("StrUtil: getWithVariations: No variations matched. Returning default: {}", defaultValue); + return defaultValue; + } + + public static String getWithNormalized(@NonNull Map configuration, @NonNull String key) { + return getWithNormalized(configuration, key, null); + } + + public static String getWithNormalized(@NonNull Map configuration, @NonNull String key, String defaultValue) { + log.trace("StrUtil: getWithNormalized: BEGIN: key={}, default={}, map={}", key, defaultValue, configuration); + + // Normalize key + String normalizedForm = StrUtil.getNormalizedForm(key, true); + log.trace("StrUtil: getWithNormalized: Normalized key form: {}", normalizedForm); + + // Search for value + for (String k : configuration.keySet()) { + String normalizedKey = StrUtil.getNormalizedForm(k, true); + if (normalizedForm.equals(normalizedKey)) { + log.trace("StrUtil: getWithNormalized: Key matched: name={}, value={}", k, configuration.get(k)); + return configuration.get(k); + } + } + log.trace("StrUtil: getWithNormalized: key not found. Returning default: {}", defaultValue); + return defaultValue; + } + + public static boolean compareNormalized(@NonNull String key1, @NonNull String key2) { + log.trace("StrUtil: compareNormalized: BEGIN: key1={}, key2={}", key1, key2); + if (key1.equals(key2)) { + log.trace("StrUtil: compareNormalized: END: Original keys are equal"); + return true; + } + + // Normalized keys + String normalizedKey1 = StrUtil.getNormalizedForm(key1, true); + String normalizedKey2 = StrUtil.getNormalizedForm(key2, true); + log.trace("StrUtil: compareNormalized: Normalized keys: key1={}, key2={}", key1, key2); + + // Compare keys + boolean areEqual = normalizedKey1.equals(normalizedKey2); + log.trace("StrUtil: compareNormalized: END: Normalized keys are {}", areEqual ? "EQUAL" : "NOT EQUAL"); + return areEqual; + } + + // ------------------------------------------------------------------------ + // Convert string to primitives + // ------------------------------------------------------------------------ + + protected static class StrConverter { + public T convert(String str, T defaultValue, Function converter, Predicate checker, boolean throwException, String exceptionMessage) { + T result = defaultValue; + if (StringUtils.isNotBlank(str)) { + try { + result = converter.apply(str.trim()); + if (checker!=null && ! checker.test(result)) { + if (throwException) + throw new IllegalArgumentException("Value check failed: str="+str); + log.warn("StrConverter: Value check failed. Default value will be returned: str={}, default={}", str, defaultValue); + result = defaultValue; + } + } catch (Exception e) { + if (throwException) + throw new IllegalArgumentException("Invalid value: str="+str, e); + String formatter = exceptionMessage; + if (StringUtils.isBlank(exceptionMessage)) { + String typeName; + if (result!=null) { + typeName = result.getClass().getSimpleName(); + } else { + /*List dummy = new ArrayList<>(0); + Type[] actualTypeArguments = ((ParameterizedType) dummy.getClass().getGenericSuperclass()).getActualTypeArguments(); + Type clazz = actualTypeArguments[0]; + Class theClass = (Class) clazz.getClass(); + typeName = theClass.getSimpleName(); + */ + typeName = "unknown_type"; + } + formatter = String.format("StrConverter: Invalid %s value: str=%s, Exception: ", typeName, str); + } + log.warn(formatter, e); + } + } + return result; + } + } + + protected final static StrConverter strToIntConverter = new StrConverter<>(); + protected final static StrConverter strToLongConverter = new StrConverter<>(); + protected final static StrConverter strToDoubleConverter = new StrConverter<>(); + + public static int strToInt(String str, int defaultValue, Predicate checker, boolean throwException, String exceptionMessage) { + return strToIntConverter.convert(str, defaultValue, Integer::parseInt, checker, throwException, exceptionMessage); + } + + public static long strToLong(String str, long defaultValue, Predicate checker, boolean throwException, String exceptionMessage) { + return strToLongConverter.convert(str, defaultValue, Long::parseLong, checker, throwException, exceptionMessage); + } + + public static double strToDouble(String str, double defaultValue, Predicate checker, boolean throwException, String exceptionMessage) { + return strToDoubleConverter.convert(str, defaultValue, Double::parseDouble, checker, throwException, exceptionMessage); + } + + public static > T strToEnum(String str, Class enumType, T defaultValue, boolean throwException, String exceptionMessage) { + String formatter = StringUtils.isNotBlank(exceptionMessage) + ? exceptionMessage : "strToEnum: Invalid enum "+enumType.getSimpleName()+" value: str={}, Exception: "; + StrConverter converter = new StrConverter<>(); + return converter.convert(str, defaultValue, (s)->Enum.valueOf(enumType, s), null, throwException, formatter); + } + + // ------------------------------------------------------------------------ + // Convert Exceptions to details string + // ------------------------------------------------------------------------ + + public static String exceptionToDetailsString(Throwable t) { + return exceptionToDetailsString(t, true, true, false, "; ", ": "); + } + + public static String exceptionToDetailsString(Throwable t, boolean printRootCauseFirst) { + return exceptionToDetailsString(t, true, true, printRootCauseFirst, "; ", ": "); + } + + public static String exceptionToDetailsString(Throwable t, + boolean printExceptionClass, + boolean printExceptionMessage, + boolean printRootCauseFirst, + String exceptionDelimiter, + String messageDelimiter) + { + if (!printExceptionClass && !printExceptionMessage) + return null; + + StringBuilder s = new StringBuilder(); + String _m = t.getMessage(); + String _d = null; + if (printExceptionMessage && StringUtils.isNotBlank(_m)) + s.append(_d = _m); + if (printExceptionClass) + s.insert(0, _d == null ? "" : messageDelimiter).insert(0, t.getClass().getName()); + + Throwable _t = t.getCause(); + //if (_t==null) return null; + if (printRootCauseFirst) { + while (_t != null) { + _m = _t.getMessage(); + _d = null; + s.insert(0, exceptionDelimiter); + if (printExceptionMessage && StringUtils.isNotBlank(_m)) + s.insert(0, _d = _m); + if (printExceptionClass) + s.insert(0, _d == null ? "" : messageDelimiter).insert(0, _t.getClass().getName()); + _t = _t.getCause(); + } + } else { + while (_t != null) { + _m = _t.getMessage(); + _d = null; + s.append(exceptionDelimiter); + if (printExceptionClass) + s.append(_d = _t.getClass().getName()); + if (printExceptionMessage && StringUtils.isNotBlank(_m)) + s.append(_d==null ? "" : messageDelimiter).append(_m); + _t = _t.getCause(); + } + } + return s.toString(); + } + + // ------------------------------------------------------------------------ + // Object Map-to-String Map conversion methods + // ------------------------------------------------------------------------ + + @SuppressWarnings("unchecked") + public static Map castToMapStringObject(Object o) { + return (Map) o; + } + + @SuppressWarnings("unchecked") + public static EventBus castToEventBusStringObjectObject(Object o) { + return (EventBus) o; + } + + public static Map deepStringifyMap(Map inputMap) { + Map outMap = new LinkedHashMap<>(); + for (Map.Entry entry : inputMap.entrySet()) { + if (entry.getValue()!=null && entry.getValue() instanceof Map) { + Map tmpMap = deepStringifyMap(castToMapStringObject(entry.getValue())); + outMap.put(entry.getKey(), tmpMap); + } else { + outMap.put(entry.getKey(), entry.getValue()!=null ? entry.getValue().toString() : null); + } + } + return outMap; + } + + public static Map deepFlattenMap(Map inputMap) { + return deepFlattenMap(inputMap, ""); + } + + public static Map deepFlattenMap(Map inputMap, String prefix) { + if (inputMap==null) + return Collections.emptyMap(); + Map outMap = new LinkedHashMap<>(); + for (Map.Entry entry : inputMap.entrySet()) { + String newKey = prefix.isEmpty() + ? entry.getKey() + : (entry.getKey()!=null) ? prefix+"."+entry.getKey() : prefix; + if (entry.getValue()!=null && entry.getValue() instanceof Map) { + Map tmpMap = deepFlattenMap(castToMapStringObject(entry.getValue()), newKey); + outMap.putAll(tmpMap); + } else { + outMap.put(newKey, entry.getValue()!=null ? entry.getValue().toString() : null); + } + } + return outMap; + } + + // ------------------------------------------------------------------------ + // Main for command-line use + // ------------------------------------------------------------------------ + + public static void main(String[] args) { + boolean uc = false; + String key = null; + String delims = null; + + for (String s : args) { + if ("-u".equals(s)) uc = true; + else if (s.startsWith("-D")) delims = s.substring(2); + else key = s; + } + + Set v; + if (uc) { + if (delims!=null) v = getVariations(key, uc, delims); + else v = getVariations(key, uc); + } else { + if (delims!=null) v = getVariations(key, delims); + else v = getVariations(key); + } + + System.out.println("> "+v); + // with Original key + v.add(key); + System.out.println("> "+v); + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/AsterisksPasswordEncoder.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/AsterisksPasswordEncoder.java new file mode 100644 index 0000000..6d2d221 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/AsterisksPasswordEncoder.java @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util.password; + +public class AsterisksPasswordEncoder implements PasswordEncoder { + public String encode(String password) { + return "********"; + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/IdentityPasswordEncoder.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/IdentityPasswordEncoder.java new file mode 100644 index 0000000..c79c7e6 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/IdentityPasswordEncoder.java @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util.password; + +public class IdentityPasswordEncoder implements PasswordEncoder { + public String encode(String password) { + return password; + } +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/PasswordEncoder.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/PasswordEncoder.java new file mode 100644 index 0000000..cea7ab4 --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/PasswordEncoder.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util.password; + +public interface PasswordEncoder { + String encode(String password); +} diff --git a/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/PresentPasswordEncoder.java b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/PresentPasswordEncoder.java new file mode 100644 index 0000000..f25a60a --- /dev/null +++ b/ems-core/util/src/main/java/gr/iccs/imu/ems/util/password/PresentPasswordEncoder.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +package gr.iccs.imu.ems.util.password; + +import org.apache.commons.lang3.StringUtils; + +public class PresentPasswordEncoder implements PasswordEncoder { + public String encode(String password) { + return StringUtils.isEmpty(password) + ? "** No password provided **" + : "** password provided **"; + } +} diff --git a/ems-core/web-admin/.dockerignore b/ems-core/web-admin/.dockerignore new file mode 100644 index 0000000..7d2f2e8 --- /dev/null +++ b/ems-core/web-admin/.dockerignore @@ -0,0 +1,3 @@ +.idea +dist +node_modules diff --git a/ems-core/web-admin/.gitignore b/ems-core/web-admin/.gitignore new file mode 100644 index 0000000..c382ea5 --- /dev/null +++ b/ems-core/web-admin/.gitignore @@ -0,0 +1,26 @@ +.DS_Store +node_modules +/dist +/node +/.env +/package-lock.json + + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/ems-core/web-admin/Dockerfile b/ems-core/web-admin/Dockerfile new file mode 100644 index 0000000..67abc32 --- /dev/null +++ b/ems-core/web-admin/Dockerfile @@ -0,0 +1,25 @@ +# +# Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless +# Esper library is used, in which case it is subject to the terms of General Public License v2.0. +# If a copy of the MPL was not distributed with this file, you can obtain one at +# https://www.mozilla.org/en-US/MPL/2.0/ +# + +FROM node:14-alpine + +ENV WEB_BASEDIR /opt/ems-web-admin + +WORKDIR ${WEB_BASEDIR} + +ADD public ./public +ADD src ./src +ADD .env . +ADD *.js . +ADD *.json . +ADD README.md . + +RUN npm install + +ENTRYPOINT ["npm", "run", "serve"] \ No newline at end of file diff --git a/ems-core/web-admin/README.md b/ems-core/web-admin/README.md new file mode 100644 index 0000000..19bd949 --- /dev/null +++ b/ems-core/web-admin/README.md @@ -0,0 +1,33 @@ + + +# ems-web-admin + +## Project setup +``` +npm install +``` + +### Compiles and hot-reloads for development +``` +npm run serve +``` + +### Compiles and minifies for production +``` +npm run build +``` + +### Lints and fixes files +``` +npm run lint +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/ems-core/web-admin/babel.config.js b/ems-core/web-admin/babel.config.js new file mode 100644 index 0000000..e955840 --- /dev/null +++ b/ems-core/web-admin/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/ems-core/web-admin/package.json b/ems-core/web-admin/package.json new file mode 100644 index 0000000..4cfebba --- /dev/null +++ b/ems-core/web-admin/package.json @@ -0,0 +1,63 @@ +{ + "name": "ems-web-admin", + "version": "1.5.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "@tip2tail/jqvmap": "^1.6.0", + "ace-builds": "^1.23.4", + "chart.js": "^3.7.1", + "core-js": "^3.21.1", + "jquery": "^3.6.0", + "jquery-knob": "^1.2.11", + "jquery-sparkline": "^2.4.0", + "jquery-ui": "^1.13.1", + "jvectormap-content": "^0.1.0", + "jvectormap-next": "^3.1.1", + "leaflet": "^1.7.1", + "leaflet-providers": "^1.13.0", + "leaflet.markercluster": "^1.5.3", + "mime-types": "^2.1.35", + "mitt": "^3.0.0", + "vue": "^3.2.31", + "vue-gauge": "^1.0.3", + "vue-json-pretty": "^2.0.6", + "vue-router": "^4.0.14", + "vue-world-map": "^0.1.1", + "vue3-ace-editor": "^2.2.2", + "vue3-blocks-tree": "^0.5.2", + "vue3-easy-data-table": "^1.5.47" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^4.5.17", + "@vue/cli-plugin-eslint": "^4.5.17", + "@vue/cli-service": "^4.5.17", + "@vue/compiler-sfc": "^3.2.31", + "babel-eslint": "^10.1.0", + "eslint": "^6.7.2", + "eslint-plugin-vue": "^7.20.0" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/vue3-essential", + "eslint:recommended" + ], + "parserOptions": { + "parser": "babel-eslint" + }, + "rules": {} + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ] +} diff --git a/ems-core/web-admin/pom.xml b/ems-core/web-admin/pom.xml new file mode 100644 index 0000000..a051450 --- /dev/null +++ b/ems-core/web-admin/pom.xml @@ -0,0 +1,178 @@ + + + 4.0.0 + + + gr.iccs.imu.ems + ems-core + ${revision} + + + web-admin + pom + EMS - Web Admin + + + v14.17.3 + + + + + build-web-admin + + false + + ../.dev-skip-build-web-admin + + + + + + + + maven-clean-plugin + 3.3.1 + + + remove-dist + clean + + clean + + + + + + + ${project.basedir}/dist + + **/* + + false + + + ${project.basedir} + + .env + + false + + + + + + + + + + maven-resources-plugin + 3.2.0 + + + create-env-file + generate-resources + + copy-resources + + + ${project.basedir} + + + ${project.basedir}/src/resources + true + + + UTF-8 + + + + + + + + com.github.eirslett + frontend-maven-plugin + 1.13.4 + + + + install node and npm + + install-node-and-npm + + + ${node.version} + + + + + npm install + + npm + + + generate-resources + + + install + + + + + npm run build + + npm + + + run build + + + + + + + + + + + diff --git a/ems-core/web-admin/public/assets/css/adminlte.min.css b/ems-core/web-admin/public/assets/css/adminlte.min.css new file mode 100644 index 0000000..611eb24 --- /dev/null +++ b/ems-core/web-admin/public/assets/css/adminlte.min.css @@ -0,0 +1,12 @@ +/*! + * AdminLTE v3.1.0 + * Author: Colorlib + * Website: AdminLTE.io + * License: Open source - MIT + *//*! + * Bootstrap v4.6.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:none}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;box-shadow:0 1px 2px rgba(0,0,0,.075);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem;box-shadow:inset 0 -.1rem 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:7.5px;padding-left:7.5px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-7.5px;margin-left:-7.5px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:7.5px;padding-left:7.5px}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-webkit-flex:0 0 20%;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-order:-1;-ms-flex-order:-1;order:-1}.order-last{-webkit-order:13;-ms-flex-order:13;order:13}.order-0{-webkit-order:0;-ms-flex-order:0;order:0}.order-1{-webkit-order:1;-ms-flex-order:1;order:1}.order-2{-webkit-order:2;-ms-flex-order:2;order:2}.order-3{-webkit-order:3;-ms-flex-order:3;order:3}.order-4{-webkit-order:4;-ms-flex-order:4;order:4}.order-5{-webkit-order:5;-ms-flex-order:5;order:5}.order-6{-webkit-order:6;-ms-flex-order:6;order:6}.order-7{-webkit-order:7;-ms-flex-order:7;order:7}.order-8{-webkit-order:8;-ms-flex-order:8;order:8}.order-9{-webkit-order:9;-ms-flex-order:9;order:9}.order-10{-webkit-order:10;-ms-flex-order:10;order:10}.order-11{-webkit-order:11;-ms-flex-order:11;order:11}.order-12{-webkit-order:12;-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-webkit-flex:0 0 20%;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-order:-1;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-order:13;-ms-flex-order:13;order:13}.order-sm-0{-webkit-order:0;-ms-flex-order:0;order:0}.order-sm-1{-webkit-order:1;-ms-flex-order:1;order:1}.order-sm-2{-webkit-order:2;-ms-flex-order:2;order:2}.order-sm-3{-webkit-order:3;-ms-flex-order:3;order:3}.order-sm-4{-webkit-order:4;-ms-flex-order:4;order:4}.order-sm-5{-webkit-order:5;-ms-flex-order:5;order:5}.order-sm-6{-webkit-order:6;-ms-flex-order:6;order:6}.order-sm-7{-webkit-order:7;-ms-flex-order:7;order:7}.order-sm-8{-webkit-order:8;-ms-flex-order:8;order:8}.order-sm-9{-webkit-order:9;-ms-flex-order:9;order:9}.order-sm-10{-webkit-order:10;-ms-flex-order:10;order:10}.order-sm-11{-webkit-order:11;-ms-flex-order:11;order:11}.order-sm-12{-webkit-order:12;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-webkit-flex:0 0 20%;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-order:-1;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-order:13;-ms-flex-order:13;order:13}.order-md-0{-webkit-order:0;-ms-flex-order:0;order:0}.order-md-1{-webkit-order:1;-ms-flex-order:1;order:1}.order-md-2{-webkit-order:2;-ms-flex-order:2;order:2}.order-md-3{-webkit-order:3;-ms-flex-order:3;order:3}.order-md-4{-webkit-order:4;-ms-flex-order:4;order:4}.order-md-5{-webkit-order:5;-ms-flex-order:5;order:5}.order-md-6{-webkit-order:6;-ms-flex-order:6;order:6}.order-md-7{-webkit-order:7;-ms-flex-order:7;order:7}.order-md-8{-webkit-order:8;-ms-flex-order:8;order:8}.order-md-9{-webkit-order:9;-ms-flex-order:9;order:9}.order-md-10{-webkit-order:10;-ms-flex-order:10;order:10}.order-md-11{-webkit-order:11;-ms-flex-order:11;order:11}.order-md-12{-webkit-order:12;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-webkit-flex:0 0 20%;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-order:-1;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-order:13;-ms-flex-order:13;order:13}.order-lg-0{-webkit-order:0;-ms-flex-order:0;order:0}.order-lg-1{-webkit-order:1;-ms-flex-order:1;order:1}.order-lg-2{-webkit-order:2;-ms-flex-order:2;order:2}.order-lg-3{-webkit-order:3;-ms-flex-order:3;order:3}.order-lg-4{-webkit-order:4;-ms-flex-order:4;order:4}.order-lg-5{-webkit-order:5;-ms-flex-order:5;order:5}.order-lg-6{-webkit-order:6;-ms-flex-order:6;order:6}.order-lg-7{-webkit-order:7;-ms-flex-order:7;order:7}.order-lg-8{-webkit-order:8;-ms-flex-order:8;order:8}.order-lg-9{-webkit-order:9;-ms-flex-order:9;order:9}.order-lg-10{-webkit-order:10;-ms-flex-order:10;order:10}.order-lg-11{-webkit-order:11;-ms-flex-order:11;order:11}.order-lg-12{-webkit-order:12;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-webkit-flex:0 0 20%;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-order:-1;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-order:13;-ms-flex-order:13;order:13}.order-xl-0{-webkit-order:0;-ms-flex-order:0;order:0}.order-xl-1{-webkit-order:1;-ms-flex-order:1;order:1}.order-xl-2{-webkit-order:2;-ms-flex-order:2;order:2}.order-xl-3{-webkit-order:3;-ms-flex-order:3;order:3}.order-xl-4{-webkit-order:4;-ms-flex-order:4;order:4}.order-xl-5{-webkit-order:5;-ms-flex-order:5;order:5}.order-xl-6{-webkit-order:6;-ms-flex-order:6;order:6}.order-xl-7{-webkit-order:7;-ms-flex-order:7;order:7}.order-xl-8{-webkit-order:8;-ms-flex-order:8;order:8}.order-xl-9{-webkit-order:9;-ms-flex-order:9;order:9}.order-xl-10{-webkit-order:10;-ms-flex-order:10;order:10}.order-xl-11{-webkit-order:11;-ms-flex-order:11;order:11}.order-xl-12{-webkit-order:12;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#383f45}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#383f45}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;box-shadow:inset 0 0 0 transparent;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:inset 0 0 0 transparent}.form-control::-webkit-input-placeholder{color:#939ba2;opacity:1}.form-control::-moz-placeholder{color:#939ba2;opacity:1}.form-control:-ms-input-placeholder{color:#939ba2;opacity:1}.form-control::-ms-input-placeholder{color:#939ba2;opacity:1}.form-control::placeholder{color:#939ba2;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:2.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 0 rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:2.25rem;background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 0 rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 0 rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 0 rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:2.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 0 rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:2.25rem;background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 0 rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 0 rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 0 rgba(220,53,69,.25)}.form-inline{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:none}.btn.disabled,.btn:disabled{opacity:.65;box-shadow:none}.btn:not(:disabled):not(.disabled){cursor:pointer}.btn:not(:disabled):not(.disabled).active,.btn:not(:disabled):not(.disabled):active{box-shadow:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff;box-shadow:none}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 0 rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d;box-shadow:none}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 0 rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745;box-shadow:none}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 0 rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8;box-shadow:none}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 0 rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(58,176,195,.5)}.btn-warning{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107;box-shadow:none}.btn-warning:hover{color:#1f2d3d;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#1f2d3d;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 0 rgba(221,171,15,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#1f2d3d;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(221,171,15,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545;box-shadow:none}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 0 rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(225,83,97,.5)}.btn-light{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa;box-shadow:none}.btn-light:hover{color:#1f2d3d;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#1f2d3d;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 0 rgba(215,218,222,.5)}.btn-light.disabled,.btn-light:disabled{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#1f2d3d;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(215,218,222,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40;box-shadow:none}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 0 rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 0 rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 0 rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 0 rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 0 rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 0 rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 0 rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 0 rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 0 rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:none}.btn-link.focus,.btn-link:focus{text-decoration:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.175)}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group.show .dropdown-toggle{box-shadow:none}.btn-group.show .dropdown-toggle.btn-link{box-shadow:none}.btn-group-vertical{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:first-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label::after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:-webkit-flex;display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(2.875rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.8125rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;-webkit-print-color-adjust:exact;color-adjust:exact}.custom-control-inline{display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff;box-shadow:none}.custom-control-input:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff;box-shadow:none}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#dee2e6;border:#adb5bd solid 1px;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff;box-shadow:none}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#dee2e6;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;box-shadow:inset 0 1px 2px rgba(0,0,0,.075);-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:inset 0 1px 2px rgba(0,0,0,.075)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:none}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;box-shadow:none}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0;margin-left:0;background-color:#007bff;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem .5rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:.5rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50%/100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;-webkit-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;-webkit-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;-webkit-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;-webkit-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;-webkit-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.75);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.75)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:0 solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 0);border-top-right-radius:calc(.25rem - 0)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 0);border-bottom-left-radius:calc(.25rem - 0)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:0 solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 0) calc(.25rem - 0) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:0 solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 0) calc(.25rem - 0)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 0)}.card-img,.card-img-bottom,.card-img-top{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 0);border-top-right-radius:calc(.25rem - 0)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 0);border-bottom-left-radius:calc(.25rem - 0)}.card-deck .card{margin-bottom:7.5px}@media (min-width:576px){.card-deck{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-7.5px;margin-left:-7.5px}.card-deck .card{-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;margin-right:7.5px;margin-bottom:0;margin-left:7.5px}}.card-group>.card{margin-bottom:7.5px}@media (min-width:576px){.card-group{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:0}.breadcrumb{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#1f2d3d;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#1f2d3d;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#1f2d3d;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#1f2d3d;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close,.alert-dismissible .mailbox-attachment-close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-flex;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem;box-shadow:inset 0 .1rem .1rem rgba(0,0,0,.1)}.progress-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close,.mailbox-attachment-close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover,.mailbox-attachment-close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover,.mailbox-attachment-close:not(:disabled):not(.disabled):focus,.mailbox-attachment-close:not(:disabled):not(.disabled):hover{opacity:.75}button.close,button.mailbox-attachment-close{padding:0;background-color:transparent;border:0}a.close.disabled,a.disabled.mailbox-attachment-close{pointer-events:none}.toast{-webkit-flex-basis:350px;-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-webkit-flex;display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close,.modal-header .mailbox-attachment-close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #e9ecef;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-content{box-shadow:0 .5rem 1rem rgba(0,0,0,.5)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.2)}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50%/100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-webkit-flex:1 1 auto!important;-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-webkit-flex-grow:0!important;-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-webkit-flex-grow:1!important;-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-webkit-flex-shrink:0!important;-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-webkit-flex-shrink:1!important;-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-webkit-flex:1 1 auto!important;-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-webkit-flex-grow:0!important;-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-webkit-flex-grow:1!important;-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-webkit-flex-shrink:0!important;-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-webkit-flex-shrink:1!important;-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-webkit-flex:1 1 auto!important;-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-webkit-flex-grow:0!important;-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-webkit-flex-grow:1!important;-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-webkit-flex-shrink:0!important;-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-webkit-flex-shrink:1!important;-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-webkit-flex:1 1 auto!important;-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-webkit-flex-grow:0!important;-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-webkit-flex-grow:1!important;-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-webkit-flex-shrink:0!important;-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-webkit-flex-shrink:1!important;-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-webkit-flex:1 1 auto!important;-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-webkit-flex-grow:0!important;-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-webkit-flex-grow:1!important;-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-webkit-flex-shrink:0!important;-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-webkit-flex-shrink:1!important;-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadeOut{from{opacity:1}to{opacity:0}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}@-webkit-keyframes shake{0%{-webkit-transform:translate(2px,1px) rotate(0);transform:translate(2px,1px) rotate(0)}10%{-webkit-transform:translate(-1px,-2px) rotate(-2deg);transform:translate(-1px,-2px) rotate(-2deg)}20%{-webkit-transform:translate(-3px,0) rotate(3deg);transform:translate(-3px,0) rotate(3deg)}30%{-webkit-transform:translate(0,2px) rotate(0);transform:translate(0,2px) rotate(0)}40%{-webkit-transform:translate(1px,-1px) rotate(1deg);transform:translate(1px,-1px) rotate(1deg)}50%{-webkit-transform:translate(-1px,2px) rotate(-1deg);transform:translate(-1px,2px) rotate(-1deg)}60%{-webkit-transform:translate(-3px,1px) rotate(0);transform:translate(-3px,1px) rotate(0)}70%{-webkit-transform:translate(2px,1px) rotate(-2deg);transform:translate(2px,1px) rotate(-2deg)}80%{-webkit-transform:translate(-1px,-1px) rotate(4deg);transform:translate(-1px,-1px) rotate(4deg)}90%{-webkit-transform:translate(2px,2px) rotate(0);transform:translate(2px,2px) rotate(0)}100%{-webkit-transform:translate(1px,-2px) rotate(-1deg);transform:translate(1px,-2px) rotate(-1deg)}}@keyframes shake{0%{-webkit-transform:translate(2px,1px) rotate(0);transform:translate(2px,1px) rotate(0)}10%{-webkit-transform:translate(-1px,-2px) rotate(-2deg);transform:translate(-1px,-2px) rotate(-2deg)}20%{-webkit-transform:translate(-3px,0) rotate(3deg);transform:translate(-3px,0) rotate(3deg)}30%{-webkit-transform:translate(0,2px) rotate(0);transform:translate(0,2px) rotate(0)}40%{-webkit-transform:translate(1px,-1px) rotate(1deg);transform:translate(1px,-1px) rotate(1deg)}50%{-webkit-transform:translate(-1px,2px) rotate(-1deg);transform:translate(-1px,2px) rotate(-1deg)}60%{-webkit-transform:translate(-3px,1px) rotate(0);transform:translate(-3px,1px) rotate(0)}70%{-webkit-transform:translate(2px,1px) rotate(-2deg);transform:translate(2px,1px) rotate(-2deg)}80%{-webkit-transform:translate(-1px,-1px) rotate(4deg);transform:translate(-1px,-1px) rotate(4deg)}90%{-webkit-transform:translate(2px,2px) rotate(0);transform:translate(2px,2px) rotate(0)}100%{-webkit-transform:translate(1px,-2px) rotate(-1deg);transform:translate(1px,-2px) rotate(-1deg)}}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{-webkit-transform:none;transform:none}}.dark-mode :root{--lightblue:#86bad8;--navy:#002c59;--olive:#74c8a3;--lime:#67ffa9;--fuchsia:#f672d8;--maroon:#ed6c9b;--blue:#3f6791;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#e74c3c;--orange:#fd7e14;--yellow:#f39c12;--green:#00bc8c;--teal:#20c997;--cyan:#3498db;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#3f6791;--secondary:#6c757d;--success:#00bc8c;--info:#3498db;--warning:#f39c12;--danger:#e74c3c;--light:#f8f9fa;--dark:#343a40}.animation__shake{-webkit-animation:shake 1.5s;animation:shake 1.5s}.animation__wobble{-webkit-animation:wobble 1.5s;animation:wobble 1.5s}.preloader{display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:#f4f6f9;height:100vh;width:100%;transition:height .2s linear;position:fixed;left:0;top:0;z-index:9999}.dark-mode .preloader{background-color:#454d55!important;color:#fff}html.scroll-smooth{scroll-behavior:smooth}.wrapper,body,html{min-height:100%}.wrapper{position:relative}.wrapper .content-wrapper{min-height:calc(100vh - calc(3.5rem + 1px) - calc(3.5rem + 1px))}.layout-boxed .wrapper{box-shadow:0 0 10 rgba(0,0,0,.3)}.layout-boxed .wrapper,.layout-boxed .wrapper::before{margin:0 auto;max-width:1250px}.layout-boxed .wrapper .main-sidebar{left:inherit}@supports not (-webkit-touch-callout:none){.layout-fixed .wrapper .sidebar{height:calc(100vh - (3.5rem + 1px))}.layout-fixed.text-sm .wrapper .sidebar{height:calc(100vh - (2.93725rem + 1px))}}.layout-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed.sidebar-mini-md.sidebar-collapse .wrapper .brand-link,.layout-navbar-fixed.sidebar-mini-xs.sidebar-collapse .wrapper .brand-link,.layout-navbar-fixed.sidebar-mini.sidebar-collapse .wrapper .brand-link{height:calc(3.5rem + 1px);width:4.6rem}.layout-navbar-fixed.sidebar-mini-md.sidebar-collapse .wrapper .brand-link.text-sm,.layout-navbar-fixed.sidebar-mini-xs.sidebar-collapse .wrapper .brand-link.text-sm,.layout-navbar-fixed.sidebar-mini.sidebar-collapse .wrapper .brand-link.text-sm{height:calc(2.93725rem + 1px)}.layout-navbar-fixed.sidebar-mini-md.sidebar-collapse.text-sm .wrapper .brand-link,.layout-navbar-fixed.sidebar-mini-xs.sidebar-collapse.text-sm .wrapper .brand-link,.layout-navbar-fixed.sidebar-mini.sidebar-collapse.text-sm .wrapper .brand-link{height:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .control-sidebar{top:0}.layout-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-navbar-fixed .wrapper .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1033}.layout-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-navbar-not-fixed .wrapper .brand-link{position:static}.layout-navbar-not-fixed .wrapper .content-wrapper,.layout-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-navbar-not-fixed .wrapper .main-header{position:static}.layout-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}.layout-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .control-sidebar{top:0}.layout-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-not-fixed .wrapper .brand-link{position:static}.layout-navbar-not-fixed .wrapper .content-wrapper,.layout-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-navbar-not-fixed .wrapper .main-header{position:static}.layout-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}@media (min-width:576px){.layout-sm-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-sm-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-sm-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-sm-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-sm-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed .wrapper .control-sidebar{top:0}.layout-sm-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-sm-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-sm-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-sm-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-sm-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-sm-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-sm-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-sm-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-sm-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-sm-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-sm-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-not-fixed .wrapper .brand-link{position:static}.layout-sm-navbar-not-fixed .wrapper .content-wrapper,.layout-sm-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-sm-navbar-not-fixed .wrapper .main-header{position:static}.layout-sm-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}@media (min-width:768px){.layout-md-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-md-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-md-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-md-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-md-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed .wrapper .control-sidebar{top:0}.layout-md-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-md-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-md-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-md-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-md-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-md-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-md-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-md-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-md-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-md-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-md-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-md-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-not-fixed .wrapper .brand-link{position:static}.layout-md-navbar-not-fixed .wrapper .content-wrapper,.layout-md-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-md-navbar-not-fixed .wrapper .main-header{position:static}.layout-md-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}@media (min-width:992px){.layout-lg-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-lg-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-lg-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-lg-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-lg-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed .wrapper .control-sidebar{top:0}.layout-lg-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-lg-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-lg-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-lg-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-lg-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-lg-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-lg-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-lg-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-lg-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-lg-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-lg-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-not-fixed .wrapper .brand-link{position:static}.layout-lg-navbar-not-fixed .wrapper .content-wrapper,.layout-lg-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-lg-navbar-not-fixed .wrapper .main-header{position:static}.layout-lg-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}@media (min-width:1200px){.layout-xl-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-xl-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-xl-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-xl-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-xl-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed .wrapper .control-sidebar{top:0}.layout-xl-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-xl-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-xl-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-xl-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-xl-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-xl-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-xl-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-xl-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-xl-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-xl-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-xl-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-not-fixed .wrapper .brand-link{position:static}.layout-xl-navbar-not-fixed .wrapper .content-wrapper,.layout-xl-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-xl-navbar-not-fixed .wrapper .main-header{position:static}.layout-xl-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}.layout-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-footer-not-fixed .wrapper .main-footer{position:static}.layout-footer-not-fixed .wrapper .content-wrapper{margin-bottom:0}.layout-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-footer-not-fixed .wrapper .main-footer{position:static}@media (min-width:576px){.layout-sm-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-sm-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-sm-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-sm-footer-not-fixed .wrapper .main-footer{position:static}}@media (min-width:768px){.layout-md-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-md-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-md-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-md-footer-not-fixed .wrapper .main-footer{position:static}}@media (min-width:992px){.layout-lg-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-lg-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-lg-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-lg-footer-not-fixed .wrapper .main-footer{position:static}}@media (min-width:1200px){.layout-xl-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-xl-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-xl-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-xl-footer-not-fixed .wrapper .main-footer{position:static}}.layout-top-nav .wrapper{margin-left:0}.layout-top-nav .wrapper .main-header .brand-image{margin-top:-.5rem;margin-right:.2rem;height:33px}.layout-top-nav .wrapper .main-sidebar{bottom:inherit;height:inherit}.layout-top-nav .wrapper .content-wrapper,.layout-top-nav .wrapper .main-footer,.layout-top-nav .wrapper .main-header{margin-left:0}body.sidebar-collapse:not(.sidebar-mini-xs):not(.sidebar-mini-md):not(.sidebar-mini) .content-wrapper,body.sidebar-collapse:not(.sidebar-mini-xs):not(.sidebar-mini-md):not(.sidebar-mini) .content-wrapper::before,body.sidebar-collapse:not(.sidebar-mini-xs):not(.sidebar-mini-md):not(.sidebar-mini) .main-footer,body.sidebar-collapse:not(.sidebar-mini-xs):not(.sidebar-mini-md):not(.sidebar-mini) .main-footer::before,body.sidebar-collapse:not(.sidebar-mini-xs):not(.sidebar-mini-md):not(.sidebar-mini) .main-header,body.sidebar-collapse:not(.sidebar-mini-xs):not(.sidebar-mini-md):not(.sidebar-mini) .main-header::before{margin-left:0}@media (min-width:768px){body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .content-wrapper,body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-footer,body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-header{transition:margin-left .3s ease-in-out;margin-left:250px}}@media (min-width:768px) and (prefers-reduced-motion:reduce){body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .content-wrapper,body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-footer,body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-header{transition:none}}@media (min-width:768px){.sidebar-collapse body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .content-wrapper,.sidebar-collapse body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-footer,.sidebar-collapse body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-header{margin-left:0}}@media (max-width:991.98px){body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .content-wrapper,body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-footer,body:not(.sidebar-mini-md):not(.sidebar-mini-xs):not(.layout-top-nav) .main-header{margin-left:0}}@media (min-width:768px){.sidebar-mini-md .content-wrapper,.sidebar-mini-md .main-footer,.sidebar-mini-md .main-header{transition:margin-left .3s ease-in-out;margin-left:250px}}@media (min-width:768px) and (prefers-reduced-motion:reduce){.sidebar-mini-md .content-wrapper,.sidebar-mini-md .main-footer,.sidebar-mini-md .main-header{transition:none}}@media (min-width:768px){.sidebar-collapse .sidebar-mini-md .content-wrapper,.sidebar-collapse .sidebar-mini-md .main-footer,.sidebar-collapse .sidebar-mini-md .main-header{margin-left:4.6rem}}@media (max-width:991.98px){.sidebar-mini-md .content-wrapper,.sidebar-mini-md .main-footer,.sidebar-mini-md .main-header{margin-left:4.6rem}}@media (max-width:767.98px){.sidebar-mini-md .content-wrapper,.sidebar-mini-md .main-footer,.sidebar-mini-md .main-header{margin-left:0}}@media (min-width:768px){.sidebar-mini-xs .content-wrapper,.sidebar-mini-xs .main-footer,.sidebar-mini-xs .main-header{transition:margin-left .3s ease-in-out;margin-left:250px}}@media (min-width:768px) and (prefers-reduced-motion:reduce){.sidebar-mini-xs .content-wrapper,.sidebar-mini-xs .main-footer,.sidebar-mini-xs .main-header{transition:none}}@media (min-width:768px){.sidebar-collapse .sidebar-mini-xs .content-wrapper,.sidebar-collapse .sidebar-mini-xs .main-footer,.sidebar-collapse .sidebar-mini-xs .main-header{margin-left:4.6rem}}@media (max-width:991.98px){.sidebar-mini-xs .content-wrapper,.sidebar-mini-xs .main-footer,.sidebar-mini-xs .main-header{margin-left:4.6rem}}.content-wrapper{background-color:#f4f6f9}.content-wrapper>.content{padding:0 .5rem}.main-sidebar,.main-sidebar::before{transition:margin-left .3s ease-in-out,width .3s ease-in-out;width:250px}@media (prefers-reduced-motion:reduce){.main-sidebar,.main-sidebar::before{transition:none}}.sidebar-collapse:not(.sidebar-mini):not(.sidebar-mini-md):not(.sidebar-mini-xs) .main-sidebar,.sidebar-collapse:not(.sidebar-mini):not(.sidebar-mini-md):not(.sidebar-mini-xs) .main-sidebar::before{box-shadow:none!important}.sidebar-collapse .main-sidebar,.sidebar-collapse .main-sidebar::before{margin-left:-250px}.sidebar-collapse .main-sidebar .nav-sidebar.nav-child-indent .nav-treeview{padding:0}@media (max-width:767.98px){.main-sidebar,.main-sidebar::before{box-shadow:none!important;margin-left:-250px}.sidebar-open .main-sidebar,.sidebar-open .main-sidebar::before{margin-left:0}}body:not(.layout-fixed) .main-sidebar{height:inherit;min-height:100%;position:absolute;top:0}body:not(.layout-fixed) .main-sidebar .sidebar{overflow-y:auto}.layout-fixed .brand-link{width:250px}.layout-fixed .main-sidebar{bottom:0;float:none;left:0;position:fixed;top:0}.layout-fixed .control-sidebar{bottom:0;float:none;position:fixed;top:0}.layout-fixed .control-sidebar .control-sidebar-content::-webkit-scrollbar{width:.5rem;height:.5rem}.layout-fixed .control-sidebar .control-sidebar-content::-webkit-scrollbar-thumb{background-color:#a9a9a9}.layout-fixed .control-sidebar .control-sidebar-content::-webkit-scrollbar-track{background-color:transparent}.layout-fixed .control-sidebar .control-sidebar-content::-webkit-scrollbar-corner{background-color:transparent}.layout-fixed .control-sidebar .control-sidebar-content{height:calc(100vh - calc(3.5rem + 1px));overflow-y:auto;-ms-overflow-style:-ms-autohiding-scrollbar;scrollbar-width:thin;scrollbar-color:#a9a9a9 transparent}@supports (-webkit-touch-callout:none){.layout-fixed .main-sidebar{height:inherit}}.main-footer{background-color:#fff;border-top:1px solid #dee2e6;color:#869099;padding:1rem}.main-footer.text-sm,.text-sm .main-footer{padding:.812rem}.content-header{padding:15px .5rem}.text-sm .content-header{padding:10px .5rem}.content-header h1{font-size:1.8rem;margin:0}.text-sm .content-header h1{font-size:1.5rem}.content-header .breadcrumb{background-color:transparent;line-height:1.8rem;margin-bottom:0;padding:0}.text-sm .content-header .breadcrumb{line-height:1.5rem}.hold-transition .content-wrapper,.hold-transition .control-sidebar,.hold-transition .control-sidebar *,.hold-transition .main-footer,.hold-transition .main-header,.hold-transition .main-sidebar,.hold-transition .main-sidebar *{transition:none!important;-webkit-animation-duration:0s!important;animation-duration:0s!important}.dark-mode{background-color:#454d55!important;color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .dark-mode .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .dark-mode .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}@media (min-width:576px){.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .dark-mode .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}}@media (min-width:768px){.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .dark-mode .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}}@media (min-width:992px){.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .dark-mode .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}}@media (min-width:1200px){.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-dark-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .dark-mode .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}}.dark-mode .breadcrumb-item+.breadcrumb-item::before,.dark-mode .breadcrumb-item.active{color:#adb5bd}.dark-mode .main-footer{background-color:#343a40;border-color:#4b545c}.dark-mode .content-wrapper{background-color:#454d55;color:#fff}.dark-mode .content-wrapper .content-header{color:#fff}.main-header{border-bottom:1px solid #dee2e6;z-index:1034}.main-header .nav-link{height:2.5rem;position:relative}.main-header.text-sm .nav-link,.text-sm .main-header .nav-link{height:1.93725rem;padding:.35rem 1rem}.main-header.text-sm .nav-link>.fa,.main-header.text-sm .nav-link>.fab,.main-header.text-sm .nav-link>.fad,.main-header.text-sm .nav-link>.fal,.main-header.text-sm .nav-link>.far,.main-header.text-sm .nav-link>.fas,.main-header.text-sm .nav-link>.ion,.main-header.text-sm .nav-link>.svg-inline--fa,.text-sm .main-header .nav-link>.fa,.text-sm .main-header .nav-link>.fab,.text-sm .main-header .nav-link>.fad,.text-sm .main-header .nav-link>.fal,.text-sm .main-header .nav-link>.far,.text-sm .main-header .nav-link>.fas,.text-sm .main-header .nav-link>.ion,.text-sm .main-header .nav-link>.svg-inline--fa{font-size:.875rem}.main-header .navbar-nav .nav-item{margin:0}.main-header .navbar-nav[class*="-right"] .dropdown-menu{left:auto;margin-top:-3px;right:0}@media (max-width:575.98px){.main-header .navbar-nav[class*="-right"] .dropdown-menu{left:0;right:auto}}.main-header.dropdown-legacy .dropdown-menu{top:3rem;margin-top:0}.navbar-img{height:calc(3.5rem + 1px)/2;width:auto}.navbar-badge{font-size:.6rem;font-weight:300;padding:2px 4px;position:absolute;right:5px;top:9px}.btn-navbar{background-color:transparent;border-left-width:0}.form-control-navbar{border-right-width:0}.form-control-navbar+.input-group-append{margin-left:0}.btn-navbar,.form-control-navbar{transition:none}.navbar-dark .btn-navbar,.navbar-dark .form-control-navbar{background-color:#343a40;border-color:#6c757d}.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar:focus,.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#495057;border-color:#6c757d!important;color:#ced4da}.navbar-light .btn-navbar,.navbar-light .form-control-navbar{background-color:#dadfe4;border-color:#ced4da}.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar::placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar:focus,.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#d3d9df;border-color:#c7ced5!important;color:#ced4da}.navbar-light .navbar-search-block .form-control-navbar:focus,.navbar-light .navbar-search-block .form-control-navbar:focus+.input-group-append .btn-navbar{color:rgba(0,0,0,.6)}.navbar-search-block{position:absolute;padding:0 1rem;left:0;top:0;right:0;bottom:0;z-index:10;display:none;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:initial}.navbar-search-block.navbar-search-open{display:-webkit-flex;display:-ms-flexbox;display:flex}.navbar-search-block .input-group{width:100%}.brand-link{display:block;font-size:1.25rem;line-height:1.5;padding:.8125rem .5rem;transition:width .3s ease-in-out;white-space:nowrap}.brand-link:hover{color:#fff;text-decoration:none}.text-sm .brand-link{font-size:inherit}[class*=sidebar-dark] .brand-link{border-bottom:1px solid #4b545c}[class*=sidebar-dark] .brand-link,[class*=sidebar-dark] .brand-link .pushmenu{color:rgba(255,255,255,.8)}[class*=sidebar-dark] .brand-link .pushmenu:hover,[class*=sidebar-dark] .brand-link:hover{color:#fff}[class*=sidebar-light] .brand-link{border-bottom:1px solid #dee2e6}[class*=sidebar-light] .brand-link,[class*=sidebar-light] .brand-link .pushmenu{color:rgba(0,0,0,.8)}[class*=sidebar-light] .brand-link .pushmenu:hover,[class*=sidebar-light] .brand-link:hover{color:#000}.brand-link .pushmenu{margin-right:.5rem;font-size:1rem}.brand-link .brand-link{padding:0;border-bottom:none}.brand-link .brand-image{float:left;line-height:.8;margin-left:.8rem;margin-right:.5rem;margin-top:-3px;max-height:33px;width:auto}.brand-link .brand-image-xs{float:left;line-height:.8;margin-top:-.1rem;max-height:33px;width:auto}.brand-link .brand-image-xl{line-height:.8;max-height:40px;width:auto}.brand-link .brand-image-xl.single{margin-top:-.3rem}.brand-link.text-sm .brand-image,.text-sm .brand-link .brand-image{height:29px;margin-bottom:-.25rem;margin-left:.95rem;margin-top:-.25rem}.brand-link.text-sm .brand-image-xs,.text-sm .brand-link .brand-image-xs{margin-top:-.2rem;max-height:29px}.brand-link.text-sm .brand-image-xl,.text-sm .brand-link .brand-image-xl{margin-top:-.225rem;max-height:38px}.main-sidebar{height:100vh;overflow-y:hidden;z-index:1038}.main-sidebar a:-moz-focusring{border:0;outline:0}.sidebar::-webkit-scrollbar{width:.5rem;height:.5rem}.sidebar::-webkit-scrollbar-thumb{background-color:#a9a9a9}.sidebar::-webkit-scrollbar-track{background-color:transparent}.sidebar::-webkit-scrollbar-corner{background-color:transparent}.sidebar{height:calc(100% - (3.5rem + 1px));overflow-x:none;overflow-y:initial;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:0;-ms-overflow-style:-ms-autohiding-scrollbar;scrollbar-width:thin;scrollbar-color:#a9a9a9 transparent}.user-panel{position:relative}[class*=sidebar-dark] .user-panel{border-bottom:1px solid #4f5962}[class*=sidebar-light] .user-panel{border-bottom:1px solid #dee2e6}.user-panel,.user-panel .info{overflow:hidden;white-space:nowrap}.user-panel .image{display:inline-block;padding-left:.8rem}.user-panel img{height:auto;width:2.1rem}.user-panel .info{display:inline-block;padding:5px 5px 5px 10px}.user-panel .dropdown-menu,.user-panel .status{font-size:.875rem}.nav-sidebar .nav-item>.nav-link{margin-bottom:.2rem}.nav-sidebar .nav-item>.nav-link .right{transition:-webkit-transform ease-in-out .3s;transition:transform ease-in-out .3s;transition:transform ease-in-out .3s,-webkit-transform ease-in-out .3s}@media (prefers-reduced-motion:reduce){.nav-sidebar .nav-item>.nav-link .right{transition:none}}.nav-sidebar .nav-link>.right,.nav-sidebar .nav-link>p>.right{position:absolute;right:1rem;top:.7rem}.nav-sidebar .nav-link>.right i,.nav-sidebar .nav-link>.right span,.nav-sidebar .nav-link>p>.right i,.nav-sidebar .nav-link>p>.right span{margin-left:.5rem}.nav-sidebar .nav-link>.right:nth-child(2),.nav-sidebar .nav-link>p>.right:nth-child(2){right:2.2rem}.nav-sidebar .menu-open>.nav-treeview{display:block}.nav-sidebar .menu-is-opening>.nav-link i.right,.nav-sidebar .menu-open>.nav-link i.right{-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.nav-sidebar>.nav-item{margin-bottom:0}.nav-sidebar>.nav-item .nav-icon{margin-left:.05rem;font-size:1.2rem;margin-right:.2rem;text-align:center;width:1.6rem}.nav-sidebar>.nav-item .nav-icon.fa,.nav-sidebar>.nav-item .nav-icon.fab,.nav-sidebar>.nav-item .nav-icon.fad,.nav-sidebar>.nav-item .nav-icon.fal,.nav-sidebar>.nav-item .nav-icon.far,.nav-sidebar>.nav-item .nav-icon.fas,.nav-sidebar>.nav-item .nav-icon.ion,.nav-sidebar>.nav-item .nav-icon.svg-inline--fa{font-size:1.1rem}.nav-sidebar>.nav-item .float-right{margin-top:3px}.nav-sidebar .nav-treeview{display:none;list-style:none;padding:0}.nav-sidebar .nav-treeview>.nav-item>.nav-link>.nav-icon{width:1.6rem}.nav-sidebar.nav-child-indent .nav-treeview{transition:padding .3s ease-in-out;padding-left:1rem}.text-sm .nav-sidebar.nav-child-indent .nav-treeview{padding-left:.5rem}.nav-sidebar.nav-child-indent.nav-legacy .nav-treeview .nav-treeview{padding-left:2rem;margin-left:-1rem}.text-sm .nav-sidebar.nav-child-indent.nav-legacy .nav-treeview .nav-treeview{padding-left:1rem;margin-left:-.5rem}.nav-sidebar .nav-header{font-size:.9rem;padding:.5rem .75rem}.nav-sidebar .nav-link p{display:inline;margin:0;white-space:normal}.sidebar-is-opening .nav-sidebar .nav-link p{-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both}#sidebar-overlay{background-color:rgba(0,0,0,.1);bottom:0;display:none;left:0;position:fixed;right:0;top:0;z-index:1037}@media (max-width:991.98px){.sidebar-open #sidebar-overlay{display:block}}[class*=sidebar-light-]{background-color:#fff}[class*=sidebar-light-] .user-panel a:hover{color:#212529}[class*=sidebar-light-] .user-panel .status{background-color:rgba(0,0,0,.1);color:#343a40}[class*=sidebar-light-] .user-panel .status:active,[class*=sidebar-light-] .user-panel .status:focus,[class*=sidebar-light-] .user-panel .status:hover{background-color:rgba(0,0,0,.1);color:#212529}[class*=sidebar-light-] .user-panel .dropdown-menu{box-shadow:0 2px 4px rgba(0,0,0,.4);border-color:rgba(0,0,0,.1)}[class*=sidebar-light-] .user-panel .dropdown-item{color:#212529}[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-link:active,[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-link:focus{color:#343a40}[class*=sidebar-light-] .nav-sidebar>.nav-item.menu-open>.nav-link,[class*=sidebar-light-] .nav-sidebar>.nav-item:hover>.nav-link{background-color:rgba(0,0,0,.1);color:#212529}[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-link.active{color:#000;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-treeview{background-color:transparent}[class*=sidebar-light-] .nav-header{background-color:inherit;color:#292d32}[class*=sidebar-light-] .sidebar a{color:#343a40}[class*=sidebar-light-] .sidebar a:hover{text-decoration:none}[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link{color:#777}[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link:focus,[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link:hover{background-color:rgba(0,0,0,.1);color:#000}[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link.active:hover{background-color:rgba(0,0,0,.1);color:#212529}[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link:hover{background-color:rgba(0,0,0,.1)}[class*=sidebar-light-] .nav-flat .nav-item .nav-treeview .nav-treeview{border-color:rgba(0,0,0,.1)}[class*=sidebar-light-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link,[class*=sidebar-light-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link.active{border-color:rgba(0,0,0,.1)}[class*=sidebar-dark-]{background-color:#343a40}[class*=sidebar-dark-] .user-panel a:hover{color:#fff}[class*=sidebar-dark-] .user-panel .status{background-color:rgba(255,255,255,.1);color:#c2c7d0}[class*=sidebar-dark-] .user-panel .status:active,[class*=sidebar-dark-] .user-panel .status:focus,[class*=sidebar-dark-] .user-panel .status:hover{background-color:rgba(247,247,247,.1);color:#fff}[class*=sidebar-dark-] .user-panel .dropdown-menu{box-shadow:0 2px 4px rgba(0,0,0,.4);border-color:rgba(242,242,242,.1)}[class*=sidebar-dark-] .user-panel .dropdown-item{color:#212529}[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-link:active{color:#c2c7d0}[class*=sidebar-dark-] .nav-sidebar>.nav-item.menu-open>.nav-link,[class*=sidebar-dark-] .nav-sidebar>.nav-item:hover>.nav-link,[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-link:focus{background-color:rgba(255,255,255,.1);color:#fff}[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-link.active{color:#fff;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-treeview{background-color:transparent}[class*=sidebar-dark-] .nav-header{background-color:inherit;color:#d0d4db}[class*=sidebar-dark-] .sidebar a{color:#c2c7d0}[class*=sidebar-dark-] .sidebar a:focus,[class*=sidebar-dark-] .sidebar a:hover{text-decoration:none}[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link{color:#c2c7d0}[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link:focus,[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link:hover{background-color:rgba(255,255,255,.1);color:#fff}[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link.active:focus,[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link.active:hover{background-color:rgba(255,255,255,.9);color:#343a40}[class*=sidebar-dark-] .nav-flat .nav-item .nav-treeview .nav-treeview{border-color:rgba(255,255,255,.9)}[class*=sidebar-dark-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link,[class*=sidebar-dark-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link.active{border-color:rgba(255,255,255,.9)}.sidebar-dark-primary .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-primary .nav-sidebar>.nav-item>.nav-link.active{background-color:#007bff;color:#fff}.sidebar-dark-primary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-primary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#007bff}.sidebar-dark-secondary .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-secondary .nav-sidebar>.nav-item>.nav-link.active{background-color:#6c757d;color:#fff}.sidebar-dark-secondary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-secondary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6c757d}.sidebar-dark-success .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-success .nav-sidebar>.nav-item>.nav-link.active{background-color:#28a745;color:#fff}.sidebar-dark-success .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-success .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#28a745}.sidebar-dark-info .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-info .nav-sidebar>.nav-item>.nav-link.active{background-color:#17a2b8;color:#fff}.sidebar-dark-info .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-info .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#17a2b8}.sidebar-dark-warning .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-warning .nav-sidebar>.nav-item>.nav-link.active{background-color:#ffc107;color:#1f2d3d}.sidebar-dark-warning .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-warning .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#ffc107}.sidebar-dark-danger .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-danger .nav-sidebar>.nav-item>.nav-link.active{background-color:#dc3545;color:#fff}.sidebar-dark-danger .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-danger .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#dc3545}.sidebar-dark-light .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-light .nav-sidebar>.nav-item>.nav-link.active{background-color:#f8f9fa;color:#1f2d3d}.sidebar-dark-light .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-light .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f8f9fa}.sidebar-dark-dark .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-dark .nav-sidebar>.nav-item>.nav-link.active{background-color:#343a40;color:#fff}.sidebar-dark-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#343a40}.sidebar-dark-lightblue .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-lightblue .nav-sidebar>.nav-item>.nav-link.active{background-color:#3c8dbc;color:#fff}.sidebar-dark-lightblue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-lightblue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3c8dbc}.sidebar-dark-navy .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-navy .nav-sidebar>.nav-item>.nav-link.active{background-color:#001f3f;color:#fff}.sidebar-dark-navy .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-navy .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#001f3f}.sidebar-dark-olive .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-olive .nav-sidebar>.nav-item>.nav-link.active{background-color:#3d9970;color:#fff}.sidebar-dark-olive .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-olive .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3d9970}.sidebar-dark-lime .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-lime .nav-sidebar>.nav-item>.nav-link.active{background-color:#01ff70;color:#1f2d3d}.sidebar-dark-lime .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-lime .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#01ff70}.sidebar-dark-fuchsia .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-fuchsia .nav-sidebar>.nav-item>.nav-link.active{background-color:#f012be;color:#fff}.sidebar-dark-fuchsia .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-fuchsia .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f012be}.sidebar-dark-maroon .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-maroon .nav-sidebar>.nav-item>.nav-link.active{background-color:#d81b60;color:#fff}.sidebar-dark-maroon .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-maroon .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#d81b60}.sidebar-dark-blue .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-blue .nav-sidebar>.nav-item>.nav-link.active{background-color:#007bff;color:#fff}.sidebar-dark-blue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-blue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#007bff}.sidebar-dark-indigo .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-indigo .nav-sidebar>.nav-item>.nav-link.active{background-color:#6610f2;color:#fff}.sidebar-dark-indigo .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-indigo .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6610f2}.sidebar-dark-purple .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-purple .nav-sidebar>.nav-item>.nav-link.active{background-color:#6f42c1;color:#fff}.sidebar-dark-purple .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-purple .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6f42c1}.sidebar-dark-pink .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-pink .nav-sidebar>.nav-item>.nav-link.active{background-color:#e83e8c;color:#fff}.sidebar-dark-pink .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-pink .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#e83e8c}.sidebar-dark-red .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-red .nav-sidebar>.nav-item>.nav-link.active{background-color:#dc3545;color:#fff}.sidebar-dark-red .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-red .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#dc3545}.sidebar-dark-orange .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-orange .nav-sidebar>.nav-item>.nav-link.active{background-color:#fd7e14;color:#1f2d3d}.sidebar-dark-orange .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-orange .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#fd7e14}.sidebar-dark-yellow .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-yellow .nav-sidebar>.nav-item>.nav-link.active{background-color:#ffc107;color:#1f2d3d}.sidebar-dark-yellow .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-yellow .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#ffc107}.sidebar-dark-green .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-green .nav-sidebar>.nav-item>.nav-link.active{background-color:#28a745;color:#fff}.sidebar-dark-green .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-green .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#28a745}.sidebar-dark-teal .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-teal .nav-sidebar>.nav-item>.nav-link.active{background-color:#20c997;color:#fff}.sidebar-dark-teal .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-teal .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#20c997}.sidebar-dark-cyan .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-cyan .nav-sidebar>.nav-item>.nav-link.active{background-color:#17a2b8;color:#fff}.sidebar-dark-cyan .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-cyan .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#17a2b8}.sidebar-dark-white .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-white .nav-sidebar>.nav-item>.nav-link.active{background-color:#fff;color:#1f2d3d}.sidebar-dark-white .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-white .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#fff}.sidebar-dark-gray .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-gray .nav-sidebar>.nav-item>.nav-link.active{background-color:#6c757d;color:#fff}.sidebar-dark-gray .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-gray .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6c757d}.sidebar-dark-gray-dark .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-gray-dark .nav-sidebar>.nav-item>.nav-link.active{background-color:#343a40;color:#fff}.sidebar-dark-gray-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-gray-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#343a40}.sidebar-mini .main-sidebar.sidebar-focused .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini .main-sidebar:not(.sidebar-no-expand) .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand) .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand) .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand):hover .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview{padding-left:1rem;margin-left:-.5rem}.nav-flat{margin:-.25rem -.5rem 0}.nav-flat .nav-item>.nav-link{border-radius:0;margin-bottom:0}.nav-flat .nav-item>.nav-link>.nav-icon{margin-left:.55rem}.nav-flat:not(.nav-child-indent) .nav-treeview .nav-item>.nav-link>.nav-icon{margin-left:.4rem}.nav-flat.nav-child-indent .nav-treeview{padding-left:0}.nav-flat.nav-child-indent .nav-treeview .nav-icon{margin-left:.85rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview{border-left:.2rem solid}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-icon{margin-left:1.15rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.45rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.75rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:2.05rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-icon{margin-left:.55rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-link{padding-left:calc(1rem - .2rem)}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-icon{margin-left:.35rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:.15rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:-.15rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:-.35rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon{margin-left:.4rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon{margin-left:.85rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon{margin-left:1.15rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.45rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.75rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-xs .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:2.05rem}.nav-flat .nav-icon{transition:margin-left ease-in-out .3s}@media (prefers-reduced-motion:reduce){.nav-flat .nav-icon{transition:none}}.nav-flat .nav-treeview .nav-icon{margin-left:-.2rem}.nav-flat.nav-sidebar>.nav-item .nav-treeview,.nav-flat.nav-sidebar>.nav-item>.nav-treeview{background-color:rgba(255,255,255,.05)}.nav-flat.nav-sidebar>.nav-item .nav-treeview .nav-item>.nav-link,.nav-flat.nav-sidebar>.nav-item>.nav-treeview .nav-item>.nav-link{border-left:.2rem solid}.nav-legacy{margin:-.25rem -.5rem 0}.nav-legacy.nav-sidebar .nav-item>.nav-link{border-radius:0;margin-bottom:0}.nav-legacy.nav-sidebar .nav-item>.nav-link>.nav-icon{margin-left:.55rem}.text-sm .nav-legacy.nav-sidebar .nav-item>.nav-link>.nav-icon{margin-left:.75rem}.nav-legacy.nav-sidebar>.nav-item>.nav-link.active{background-color:inherit;border-left:3px solid transparent;box-shadow:none}.nav-legacy.nav-sidebar>.nav-item>.nav-link.active>.nav-icon{margin-left:calc(.55rem - 3px)}.text-sm .nav-legacy.nav-sidebar>.nav-item>.nav-link.active>.nav-icon{margin-left:calc(.75rem - 3px)}.text-sm .nav-legacy.nav-sidebar.nav-flat .nav-treeview .nav-item>.nav-link>.nav-icon{margin-left:calc(.75rem - 3px)}.sidebar-mini .nav-legacy>.nav-item .nav-link .nav-icon,.sidebar-mini-md .nav-legacy>.nav-item .nav-link .nav-icon,.sidebar-mini-xs .nav-legacy>.nav-item .nav-link .nav-icon{transition:margin-left ease-in-out .3s;margin-left:.6rem}@media (prefers-reduced-motion:reduce){.sidebar-mini .nav-legacy>.nav-item .nav-link .nav-icon,.sidebar-mini-md .nav-legacy>.nav-item .nav-link .nav-icon,.sidebar-mini-xs .nav-legacy>.nav-item .nav-link .nav-icon{transition:none}}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview{padding-left:1rem}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview{padding-left:2rem;margin-left:-1rem}.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-xs.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-xs.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview{padding-left:.5rem}.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-xs.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-xs.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview{padding-left:1rem;margin-left:-.5rem}.sidebar-mini-md.sidebar-collapse .nav-legacy>.nav-item>.nav-link .nav-icon,.sidebar-mini-xs.sidebar-collapse .nav-legacy>.nav-item>.nav-link .nav-icon,.sidebar-mini.sidebar-collapse .nav-legacy>.nav-item>.nav-link .nav-icon{margin-left:.55rem}.sidebar-mini-md.sidebar-collapse .nav-legacy>.nav-item>.nav-link.active>.nav-icon,.sidebar-mini-xs.sidebar-collapse .nav-legacy>.nav-item>.nav-link.active>.nav-icon,.sidebar-mini.sidebar-collapse .nav-legacy>.nav-item>.nav-link.active>.nav-icon{margin-left:.36rem}.sidebar-mini-md.sidebar-collapse .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-xs.sidebar-collapse .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse .nav-legacy.nav-child-indent .nav-treeview .nav-treeview{padding-left:0;margin-left:0}.sidebar-mini-md.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link .nav-icon,.sidebar-mini-xs.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link .nav-icon,.sidebar-mini.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link .nav-icon{margin-left:.75rem}.sidebar-mini-md.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link.active>.nav-icon,.sidebar-mini-xs.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link.active>.nav-icon,.sidebar-mini.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link.active>.nav-icon{margin-left:calc(.75rem - 3px)}[class*=sidebar-dark] .nav-legacy.nav-sidebar>.nav-item .nav-treeview,[class*=sidebar-dark] .nav-legacy.nav-sidebar>.nav-item>.nav-treeview{background-color:rgba(255,255,255,.05)}[class*=sidebar-dark] .nav-legacy.nav-sidebar>.nav-item>.nav-link.active{color:#fff}[class*=sidebar-dark] .nav-legacy .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-dark] .nav-legacy .nav-treeview>.nav-item>.nav-link:focus,[class*=sidebar-dark] .nav-legacy .nav-treeview>.nav-item>.nav-link:hover{background-color:transparent;color:#fff}[class*=sidebar-light] .nav-legacy.nav-sidebar>.nav-item .nav-treeview,[class*=sidebar-light] .nav-legacy.nav-sidebar>.nav-item>.nav-treeview{background-color:rgba(0,0,0,.05)}[class*=sidebar-light] .nav-legacy.nav-sidebar>.nav-item>.nav-link.active{color:#000}[class*=sidebar-light] .nav-legacy .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-light] .nav-legacy .nav-treeview>.nav-item>.nav-link:focus,[class*=sidebar-light] .nav-legacy .nav-treeview>.nav-item>.nav-link:hover{background-color:transparent;color:#000}.nav-collapse-hide-child .menu-open>.nav-treeview{max-height:-webkit-min-content;max-height:-moz-min-content;max-height:min-content;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.sidebar-collapse .nav-collapse-hide-child .menu-open>.nav-treeview{max-height:0;-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini-md.sidebar-collapse .main-sidebar:not(.sidebar-no-expand):hover .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini-xs.sidebar-collapse .main-sidebar:not(.sidebar-no-expand):hover .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar:not(.sidebar-no-expand):hover .nav-collapse-hide-child .menu-open>.nav-treeview{max-height:-webkit-min-content;max-height:-moz-min-content;max-height:min-content;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.nav-compact .nav-header,.nav-compact .nav-link{padding-top:.25rem;padding-bottom:.25rem}.nav-compact .nav-header:not(:first-of-type){padding-top:.75rem;padding-bottom:.25rem}.nav-compact .nav-link>.right,.nav-compact .nav-link>p>.right{top:.465rem}.text-sm .nav-compact .nav-link>.right,.text-sm .nav-compact .nav-link>p>.right{top:.7rem}[class*=sidebar-dark] .btn-sidebar,[class*=sidebar-dark] .form-control-sidebar{background-color:#3f474e;border:1px solid #56606a;color:#fff}[class*=sidebar-dark] .btn-sidebar:focus,[class*=sidebar-dark] .form-control-sidebar:focus{border:1px solid #7a8793}[class*=sidebar-dark] .btn-sidebar:hover{background-color:#454d55}[class*=sidebar-dark] .btn-sidebar:focus{background-color:#4b545c}[class*=sidebar-dark] .list-group-item{background-color:#454d55;border-color:#56606a;color:#c2c7d0}[class*=sidebar-dark] .list-group-item:hover{background-color:#4b545c}[class*=sidebar-dark] .list-group-item:focus{background-color:#515a63}[class*=sidebar-dark] .list-group-item .search-path{color:#adb5bd}[class*=sidebar-light] .btn-sidebar,[class*=sidebar-light] .form-control-sidebar{background-color:#f2f2f2;border:1px solid #d9d9d9;color:#1f2d3d}[class*=sidebar-light] .btn-sidebar:focus,[class*=sidebar-light] .form-control-sidebar:focus{border:1px solid #b3b3b3}[class*=sidebar-light] .btn-sidebar:hover{background-color:#ececec}[class*=sidebar-light] .btn-sidebar:focus{background-color:#e6e6e6}[class*=sidebar-light] .list-group-item{border-color:#d9d9d9}[class*=sidebar-light] .list-group-item:hover{background-color:#ececec}[class*=sidebar-light] .list-group-item:focus{background-color:#e6e6e6}[class*=sidebar-light] .list-group-item .search-path{color:#6c757d}.sidebar .form-inline .input-group{width:100%;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.sidebar nav .form-inline{margin-bottom:.2rem}.layout-boxed:not(.sidebar-mini):not(.sidebar-mini-md):not(.sidebar-mini-xs).sidebar-collapse .main-sidebar{margin-left:0}.layout-boxed:not(.sidebar-mini):not(.sidebar-mini-md):not(.sidebar-mini-xs) .content-wrapper,.layout-boxed:not(.sidebar-mini):not(.sidebar-mini-md):not(.sidebar-mini-xs) .main-footer,.layout-boxed:not(.sidebar-mini):not(.sidebar-mini-md):not(.sidebar-mini-xs) .main-header{z-index:9999;position:relative}.sidebar-collapse .form-control-sidebar,.sidebar-collapse .form-control-sidebar~.input-group-append,.sidebar-collapse .sidebar-search-results{display:none}[data-widget=sidebar-search] input[type=search]::-ms-clear,[data-widget=sidebar-search] input[type=search]::-ms-reveal{display:none;width:0;height:0}[data-widget=sidebar-search] input[type=search]::-webkit-search-cancel-button,[data-widget=sidebar-search] input[type=search]::-webkit-search-decoration,[data-widget=sidebar-search] input[type=search]::-webkit-search-results-button,[data-widget=sidebar-search] input[type=search]::-webkit-search-results-decoration{display:none}.sidebar-search-results{position:relative;display:none;width:100%}.sidebar-search-open .sidebar-search-results{display:inline-block}.sidebar-search-results .search-title{margin-bottom:-.1rem}.sidebar-search-results .list-group{position:absolute;width:100%;z-index:1039}.sidebar-search-results .list-group>.list-group-item{padding:.375rem .75rem}.sidebar-search-results .list-group>.list-group-item:-moz-focusring{margin-top:0;border-left:1px solid transparent;border-top:0;border-bottom:1px solid transparent}.sidebar-search-results .list-group>.list-group-item:first-child{margin-top:0;border-top:0;border-top-left-radius:0;border-top-right-radius:0}.sidebar-search-results .search-path{font-size:80%}.sidebar-search-open .btn,.sidebar-search-open .form-control{border-bottom-right-radius:0;border-bottom-left-radius:0}[class*=sidebar-dark] .sidebar-custom{border-top:1px solid #4f5962}[class*=sidebar-light] .sidebar-custom{border-top:1px solid #dee2e6}.layout-fixed.sidebar-collapse .hide-on-collapse{display:none}.layout-fixed.sidebar-collapse:hover .hide-on-collapse{display:block}.layout-fixed .main-sidebar-custom .sidebar{height:calc(100% - ((3.5rem + 4rem) + 1px))}.layout-fixed .main-sidebar-custom .sidebar-custom{height:4rem;padding:.85rem .5rem}.layout-fixed .main-sidebar-custom-lg .sidebar{height:calc(100% - ((3.5rem + 6rem) + 1px))}.layout-fixed .main-sidebar-custom-lg .sidebar-custom{height:6rem}.layout-fixed .main-sidebar-custom-xl .sidebar{height:calc(100% - ((3.5rem + 8rem) + 1px))}.layout-fixed .main-sidebar-custom-xl .sidebar-custom{height:8rem}.layout-fixed .main-sidebar-custom .pos-right,.layout-fixed .main-sidebar-custom-lg .pos-right,.layout-fixed .main-sidebar-custom-xl .pos-right{position:absolute;right:.5rem}.dark-mode .sidebar-dark-primary .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-primary .nav-sidebar>.nav-item>.nav-link.active{background-color:#3f6791;color:#fff}.dark-mode .sidebar-dark-primary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-primary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3f6791}.dark-mode .sidebar-dark-secondary .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-secondary .nav-sidebar>.nav-item>.nav-link.active{background-color:#6c757d;color:#fff}.dark-mode .sidebar-dark-secondary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-secondary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6c757d}.dark-mode .sidebar-dark-success .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-success .nav-sidebar>.nav-item>.nav-link.active{background-color:#00bc8c;color:#fff}.dark-mode .sidebar-dark-success .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-success .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#00bc8c}.dark-mode .sidebar-dark-info .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-info .nav-sidebar>.nav-item>.nav-link.active{background-color:#3498db;color:#fff}.dark-mode .sidebar-dark-info .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-info .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3498db}.dark-mode .sidebar-dark-warning .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-warning .nav-sidebar>.nav-item>.nav-link.active{background-color:#f39c12;color:#1f2d3d}.dark-mode .sidebar-dark-warning .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-warning .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f39c12}.dark-mode .sidebar-dark-danger .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-danger .nav-sidebar>.nav-item>.nav-link.active{background-color:#e74c3c;color:#fff}.dark-mode .sidebar-dark-danger .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-danger .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#e74c3c}.dark-mode .sidebar-dark-light .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-light .nav-sidebar>.nav-item>.nav-link.active{background-color:#f8f9fa;color:#1f2d3d}.dark-mode .sidebar-dark-light .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-light .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f8f9fa}.dark-mode .sidebar-dark-dark .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-dark .nav-sidebar>.nav-item>.nav-link.active{background-color:#343a40;color:#fff}.dark-mode .sidebar-dark-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#343a40}.dark-mode .sidebar-dark-lightblue .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-lightblue .nav-sidebar>.nav-item>.nav-link.active{background-color:#86bad8;color:#1f2d3d}.dark-mode .sidebar-dark-lightblue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-lightblue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#86bad8}.dark-mode .sidebar-dark-navy .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-navy .nav-sidebar>.nav-item>.nav-link.active{background-color:#002c59;color:#fff}.dark-mode .sidebar-dark-navy .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-navy .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#002c59}.dark-mode .sidebar-dark-olive .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-olive .nav-sidebar>.nav-item>.nav-link.active{background-color:#74c8a3;color:#1f2d3d}.dark-mode .sidebar-dark-olive .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-olive .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#74c8a3}.dark-mode .sidebar-dark-lime .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-lime .nav-sidebar>.nav-item>.nav-link.active{background-color:#67ffa9;color:#1f2d3d}.dark-mode .sidebar-dark-lime .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-lime .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#67ffa9}.dark-mode .sidebar-dark-fuchsia .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-fuchsia .nav-sidebar>.nav-item>.nav-link.active{background-color:#f672d8;color:#1f2d3d}.dark-mode .sidebar-dark-fuchsia .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-fuchsia .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f672d8}.dark-mode .sidebar-dark-maroon .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-maroon .nav-sidebar>.nav-item>.nav-link.active{background-color:#ed6c9b;color:#1f2d3d}.dark-mode .sidebar-dark-maroon .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-maroon .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#ed6c9b}.dark-mode .sidebar-dark-blue .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-blue .nav-sidebar>.nav-item>.nav-link.active{background-color:#3f6791;color:#fff}.dark-mode .sidebar-dark-blue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-blue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3f6791}.dark-mode .sidebar-dark-indigo .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-indigo .nav-sidebar>.nav-item>.nav-link.active{background-color:#6610f2;color:#fff}.dark-mode .sidebar-dark-indigo .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-indigo .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6610f2}.dark-mode .sidebar-dark-purple .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-purple .nav-sidebar>.nav-item>.nav-link.active{background-color:#6f42c1;color:#fff}.dark-mode .sidebar-dark-purple .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-purple .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6f42c1}.dark-mode .sidebar-dark-pink .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-pink .nav-sidebar>.nav-item>.nav-link.active{background-color:#e83e8c;color:#fff}.dark-mode .sidebar-dark-pink .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-pink .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#e83e8c}.dark-mode .sidebar-dark-red .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-red .nav-sidebar>.nav-item>.nav-link.active{background-color:#e74c3c;color:#fff}.dark-mode .sidebar-dark-red .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-red .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#e74c3c}.dark-mode .sidebar-dark-orange .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-orange .nav-sidebar>.nav-item>.nav-link.active{background-color:#fd7e14;color:#1f2d3d}.dark-mode .sidebar-dark-orange .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-orange .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#fd7e14}.dark-mode .sidebar-dark-yellow .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-yellow .nav-sidebar>.nav-item>.nav-link.active{background-color:#f39c12;color:#1f2d3d}.dark-mode .sidebar-dark-yellow .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-yellow .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f39c12}.dark-mode .sidebar-dark-green .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-green .nav-sidebar>.nav-item>.nav-link.active{background-color:#00bc8c;color:#fff}.dark-mode .sidebar-dark-green .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-green .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#00bc8c}.dark-mode .sidebar-dark-teal .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-teal .nav-sidebar>.nav-item>.nav-link.active{background-color:#20c997;color:#fff}.dark-mode .sidebar-dark-teal .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-teal .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#20c997}.dark-mode .sidebar-dark-cyan .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-cyan .nav-sidebar>.nav-item>.nav-link.active{background-color:#3498db;color:#fff}.dark-mode .sidebar-dark-cyan .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-cyan .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3498db}.dark-mode .sidebar-dark-white .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-white .nav-sidebar>.nav-item>.nav-link.active{background-color:#fff;color:#1f2d3d}.dark-mode .sidebar-dark-white .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-white .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#fff}.dark-mode .sidebar-dark-gray .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-gray .nav-sidebar>.nav-item>.nav-link.active{background-color:#6c757d;color:#fff}.dark-mode .sidebar-dark-gray .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-gray .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6c757d}.dark-mode .sidebar-dark-gray-dark .nav-sidebar>.nav-item>.nav-link.active,.dark-mode .sidebar-light-gray-dark .nav-sidebar>.nav-item>.nav-link.active{background-color:#343a40;color:#fff}.dark-mode .sidebar-dark-gray-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.dark-mode .sidebar-light-gray-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#343a40}.dark-mode [class*=sidebar-light-] .sidebar a{color:#343a40}.dark-mode [class*=sidebar-light-] .sidebar a:hover{text-decoration:none}.logo-xl,.logo-xs{opacity:1;position:absolute;visibility:visible}.logo-xl.brand-image-xs,.logo-xs.brand-image-xs{left:18px;top:12px}.logo-xl.brand-image-xl,.logo-xs.brand-image-xl{left:12px;top:6px}.logo-xs{opacity:0;visibility:hidden}.logo-xs.brand-image-xl{left:16px;top:8px}.brand-link.logo-switch::before{content:"\00a0"}@media (min-width:992px){.sidebar-mini .nav-sidebar,.sidebar-mini .nav-sidebar .nav-link,.sidebar-mini .nav-sidebar>.nav-header{white-space:nowrap}.sidebar-mini.sidebar-collapse .d-hidden-mini{display:none}.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .main-footer,.sidebar-mini.sidebar-collapse .main-header{margin-left:4.6rem!important}.sidebar-mini.sidebar-collapse .nav-sidebar .nav-header{display:none}.sidebar-mini.sidebar-collapse .nav-sidebar .nav-link p{width:0;white-space:nowrap}.sidebar-mini.sidebar-collapse .brand-text,.sidebar-mini.sidebar-collapse .nav-sidebar .nav-link p,.sidebar-mini.sidebar-collapse .sidebar .user-panel>.info{margin-left:-10px;-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini.sidebar-collapse .logo-xl{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini.sidebar-collapse .logo-xs{display:inline-block;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}.sidebar-mini.sidebar-collapse .main-sidebar{overflow-x:hidden}.sidebar-mini.sidebar-collapse .main-sidebar,.sidebar-mini.sidebar-collapse .main-sidebar::before{margin-left:0;width:4.6rem}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel .image{float:none}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused,.sidebar-mini.sidebar-collapse .main-sidebar:hover{width:250px}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .brand-link,.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-link{width:250px}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel{text-align:left}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel .image,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel .image{float:left}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .brand-text,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .logo-xl,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-text,.sidebar-mini.sidebar-collapse .main-sidebar:hover .logo-xl,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:inline-block;margin-left:0;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .logo-xs,.sidebar-mini.sidebar-collapse .main-sidebar:hover .logo-xs{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .brand-image,.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-image{margin-right:.5rem}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .sidebar-form,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini.sidebar-collapse .main-sidebar:hover .sidebar-form,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:block!important;-webkit-transform:translateZ(0);transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar>.nav-item>.nav-link>span,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar>.nav-item>.nav-link>span{display:inline-block!important}.sidebar-mini.sidebar-collapse .visible-sidebar-mini{display:block!important}.sidebar-mini.sidebar-collapse.layout-fixed .main-sidebar:hover .brand-link{width:250px}.sidebar-mini.sidebar-collapse.layout-fixed .brand-link{width:4.6rem}}@media (max-width:991.98px){.sidebar-mini.sidebar-collapse .main-sidebar{box-shadow:none!important}}@media (min-width:768px){.sidebar-mini-md .nav-sidebar,.sidebar-mini-md .nav-sidebar .nav-link,.sidebar-mini-md .nav-sidebar>.nav-header{white-space:nowrap}.sidebar-mini-md.sidebar-collapse .d-hidden-mini{display:none}.sidebar-mini-md.sidebar-collapse .content-wrapper,.sidebar-mini-md.sidebar-collapse .main-footer,.sidebar-mini-md.sidebar-collapse .main-header{margin-left:4.6rem!important}.sidebar-mini-md.sidebar-collapse .nav-sidebar .nav-header{display:none}.sidebar-mini-md.sidebar-collapse .nav-sidebar .nav-link p{width:0;white-space:nowrap}.sidebar-mini-md.sidebar-collapse .brand-text,.sidebar-mini-md.sidebar-collapse .nav-sidebar .nav-link p,.sidebar-mini-md.sidebar-collapse .sidebar .user-panel>.info{margin-left:-10px;-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini-md.sidebar-collapse .logo-xl{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini-md.sidebar-collapse .logo-xs{display:inline-block;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}.sidebar-mini-md.sidebar-collapse .main-sidebar{overflow-x:hidden}.sidebar-mini-md.sidebar-collapse .main-sidebar,.sidebar-mini-md.sidebar-collapse .main-sidebar::before{margin-left:0;width:4.6rem}.sidebar-mini-md.sidebar-collapse .main-sidebar .user-panel .image{float:none}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover{width:250px}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .brand-link,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .brand-link{width:250px}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel{text-align:left}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel .image,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel .image{float:left}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .brand-text,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .logo-xl,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .brand-text,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .logo-xl,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:inline-block;margin-left:0;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .logo-xs,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .logo-xs{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .brand-image,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .brand-image{margin-right:.5rem}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .sidebar-form,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .sidebar-form,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:block!important;-webkit-transform:translateZ(0);transform:translateZ(0)}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar>.nav-item>.nav-link>span,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-sidebar>.nav-item>.nav-link>span{display:inline-block!important}.sidebar-mini-md.sidebar-collapse .visible-sidebar-mini{display:block!important}.sidebar-mini-md.sidebar-collapse.layout-fixed .main-sidebar:hover .brand-link{width:250px}.sidebar-mini-md.sidebar-collapse.layout-fixed .brand-link{width:4.6rem}}@media (max-width:767.98px){.sidebar-mini-md.sidebar-collapse .main-sidebar{box-shadow:none!important}}.sidebar-mini-xs .nav-sidebar,.sidebar-mini-xs .nav-sidebar .nav-link,.sidebar-mini-xs .nav-sidebar>.nav-header{white-space:nowrap}.sidebar-mini-xs.sidebar-collapse .d-hidden-mini{display:none}.sidebar-mini-xs.sidebar-collapse .content-wrapper,.sidebar-mini-xs.sidebar-collapse .main-footer,.sidebar-mini-xs.sidebar-collapse .main-header{margin-left:4.6rem!important}.sidebar-mini-xs.sidebar-collapse .nav-sidebar .nav-header{display:none}.sidebar-mini-xs.sidebar-collapse .nav-sidebar .nav-link p{width:0;white-space:nowrap}.sidebar-mini-xs.sidebar-collapse .brand-text,.sidebar-mini-xs.sidebar-collapse .nav-sidebar .nav-link p,.sidebar-mini-xs.sidebar-collapse .sidebar .user-panel>.info{margin-left:-10px;-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini-xs.sidebar-collapse .logo-xl{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini-xs.sidebar-collapse .logo-xs{display:inline-block;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}.sidebar-mini-xs.sidebar-collapse .main-sidebar{overflow-x:hidden}.sidebar-mini-xs.sidebar-collapse .main-sidebar,.sidebar-mini-xs.sidebar-collapse .main-sidebar::before{margin-left:0;width:4.6rem}.sidebar-mini-xs.sidebar-collapse .main-sidebar .user-panel .image{float:none}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover{width:250px}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .brand-link,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .brand-link{width:250px}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .user-panel,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .user-panel{text-align:left}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .user-panel .image,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .user-panel .image{float:left}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .brand-text,.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .logo-xl,.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .brand-text,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .logo-xl,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:inline-block;margin-left:0;-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .logo-xs,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .logo-xs{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .brand-image,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .brand-image{margin-right:.5rem}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .sidebar-form,.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .sidebar-form,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:block!important;-webkit-transform:translateZ(0);transform:translateZ(0)}.sidebar-mini-xs.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar>.nav-item>.nav-link>span,.sidebar-mini-xs.sidebar-collapse .main-sidebar:hover .nav-sidebar>.nav-item>.nav-link>span{display:inline-block!important}.sidebar-mini-xs.sidebar-collapse .visible-sidebar-mini{display:block!important}.sidebar-mini-xs.sidebar-collapse.layout-fixed .main-sidebar:hover .brand-link{width:250px}.sidebar-mini-xs.sidebar-collapse.layout-fixed .brand-link{width:4.6rem}.sidebar-mini .main-sidebar .nav-child-indent .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 1rem)}.sidebar-mini .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 2rem)}.sidebar-mini .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 3rem)}.sidebar-mini .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 4rem)}.sidebar-mini .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 5rem)}.sidebar-mini .main-sidebar .nav-legacy .nav-link,.sidebar-mini-md .main-sidebar .nav-legacy .nav-link,.sidebar-mini-xs .main-sidebar .nav-legacy .nav-link{width:250px}.sidebar-mini .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-link{width:calc(250px - 1rem)}.sidebar-mini .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 1rem)}.sidebar-mini .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 2rem)}.sidebar-mini .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 3rem)}.sidebar-mini .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 4rem)}.sidebar-mini .main-sidebar .nav-flat .nav-link,.sidebar-mini-md .main-sidebar .nav-flat .nav-link,.sidebar-mini-xs .main-sidebar .nav-flat .nav-link{width:250px}.sidebar-mini .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-link{width:calc(250px)}.sidebar-mini .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem)}.sidebar-mini .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem * 2)}.sidebar-mini .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem * 3)}.sidebar-mini .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem * 4)}.sidebar-mini .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - .5rem)}.sidebar-mini .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 1rem)}.sidebar-mini .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 1.5rem)}.sidebar-mini .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 2rem)}.sidebar-mini .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2 - 2.5rem)}.sidebar-mini .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-link{width:250px}.sidebar-mini .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link{width:calc(250px - .5rem)}.sidebar-mini .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2)}.sidebar-mini .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 3)}.sidebar-mini .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 4)}.sidebar-mini .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-md .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-mini-xs .main-sidebar .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 5)}.sidebar-mini .main-sidebar .nav-link,.sidebar-mini-md .main-sidebar .nav-link,.sidebar-mini-xs .main-sidebar .nav-link{width:calc(250px - .5rem * 2);transition:width ease-in-out .3s}@media (prefers-reduced-motion:reduce){.sidebar-mini .main-sidebar .nav-link,.sidebar-mini-md .main-sidebar .nav-link,.sidebar-mini-xs .main-sidebar .nav-link{transition:none}}.sidebar-collapse.sidebar-mini .main-sidebar .nav-sidebar .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar .nav-sidebar .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar .nav-sidebar .nav-link{width:3.6rem}.sidebar-collapse.sidebar-mini .main-sidebar .nav-sidebar.nav-flat .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar .nav-sidebar.nav-legacy .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar .nav-sidebar.nav-flat .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar .nav-sidebar.nav-legacy .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar .nav-sidebar.nav-flat .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar .nav-sidebar.nav-legacy .nav-link{width:4.6rem}.sidebar-collapse.sidebar-mini .main-sidebar .nav-sidebar.nav-child-indent.nav-compact .nav-treeview,.sidebar-collapse.sidebar-mini-md .main-sidebar .nav-sidebar.nav-child-indent.nav-compact .nav-treeview,.sidebar-collapse.sidebar-mini-xs .main-sidebar .nav-sidebar.nav-child-indent.nav-compact .nav-treeview{padding-left:0!important;margin-left:0!important}.sidebar-collapse.sidebar-mini .main-sidebar .nav-sidebar.nav-child-indent.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar .nav-sidebar.nav-child-indent.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar .nav-sidebar.nav-child-indent.nav-compact .nav-link{width:calc(4.6rem - .5rem * 2)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-link{width:calc(250px - .5rem * 2)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-header,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-header,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-header,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-header,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-header,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-header{display:inline-block}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent .nav-link{width:calc(250px - .5rem * 2)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-legacy .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-legacy .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-legacy .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-legacy .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-legacy .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-legacy .nav-link{width:250px}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-link{width:calc(250px - 1rem)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 1rem)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 2rem)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 3rem)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - 1rem - 4rem)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-flat .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-flat .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-flat .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-flat .nav-link{width:250px}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-link{width:calc(250px)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem * 2)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem * 3)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .2rem * 4)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent.nav-compact .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent.nav-compact .nav-link{width:calc(250px - .5rem * 2)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-link{width:250px}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-link{width:calc(250px - .5rem)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 2)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 3)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 4)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .nav-child-indent.nav-legacy.nav-compact .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-link{width:calc(250px - .5rem * 5)}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar,.sidebar-collapse.sidebar-mini .main-sidebar:hover .sidebar::-webkit-scrollbar,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .sidebar::-webkit-scrollbar,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .sidebar::-webkit-scrollbar{width:.5rem;height:.5rem}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-thumb,.sidebar-collapse.sidebar-mini .main-sidebar:hover .sidebar::-webkit-scrollbar-thumb,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-thumb,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .sidebar::-webkit-scrollbar-thumb,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-thumb,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .sidebar::-webkit-scrollbar-thumb{background-color:#a9a9a9}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-track,.sidebar-collapse.sidebar-mini .main-sidebar:hover .sidebar::-webkit-scrollbar-track,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-track,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .sidebar::-webkit-scrollbar-track,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-track,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .sidebar::-webkit-scrollbar-track{background-color:transparent}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-corner,.sidebar-collapse.sidebar-mini .main-sidebar:hover .sidebar::-webkit-scrollbar-corner,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-corner,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .sidebar::-webkit-scrollbar-corner,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .sidebar::-webkit-scrollbar-corner,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .sidebar::-webkit-scrollbar-corner{background-color:transparent}.sidebar-collapse.sidebar-mini .main-sidebar.sidebar-focused .sidebar,.sidebar-collapse.sidebar-mini .main-sidebar:hover .sidebar,.sidebar-collapse.sidebar-mini-md .main-sidebar.sidebar-focused .sidebar,.sidebar-collapse.sidebar-mini-md .main-sidebar:hover .sidebar,.sidebar-collapse.sidebar-mini-xs .main-sidebar.sidebar-focused .sidebar,.sidebar-collapse.sidebar-mini-xs .main-sidebar:hover .sidebar{-ms-overflow-style:-ms-autohiding-scrollbar;scrollbar-width:thin;scrollbar-color:#a9a9a9 transparent}.sidebar-collapse.sidebar-mini .main-sidebar .sidebar::-webkit-scrollbar,.sidebar-collapse.sidebar-mini-md .main-sidebar .sidebar::-webkit-scrollbar,.sidebar-collapse.sidebar-mini-xs .main-sidebar .sidebar::-webkit-scrollbar{width:0;height:0}.sidebar-collapse.sidebar-mini .main-sidebar .sidebar,.sidebar-collapse.sidebar-mini-md .main-sidebar .sidebar,.sidebar-collapse.sidebar-mini-xs .main-sidebar .sidebar{-ms-overflow-style:-ms-autohiding-scrollbar;scrollbar-width:none}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover{width:4.6rem}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .nav-header,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .nav-header,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .nav-header,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .nav-header,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .nav-header,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .nav-header{display:none}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .brand-link,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .brand-link,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .brand-link,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .brand-link,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .brand-link,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .brand-link{width:4.6rem!important}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel .image,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .user-panel .image,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel .image,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .user-panel .image,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel .image,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .user-panel .image{float:none!important}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xs,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .logo-xs,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xs,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .logo-xs,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xs,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .logo-xs{-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xl,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .logo-xl,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xl,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .logo-xl,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xl,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .logo-xl{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar.nav-child-indent .nav-treeview,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .nav-sidebar.nav-child-indent .nav-treeview,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar.nav-child-indent .nav-treeview,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .nav-sidebar.nav-child-indent .nav-treeview,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar.nav-child-indent .nav-treeview,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .nav-sidebar.nav-child-indent .nav-treeview{padding-left:0}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .brand-text,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel>.info,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .brand-text,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .user-panel>.info,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .brand-text,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel>.info,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .brand-text,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .user-panel>.info,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .brand-text,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel>.info,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .brand-text,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .user-panel>.info{margin-left:-10px;-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:hidden;width:0}.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar>.nav-item .nav-icon,.sidebar-collapse.sidebar-mini .sidebar-no-expand.main-sidebar:hover .nav-sidebar>.nav-item .nav-icon,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar>.nav-item .nav-icon,.sidebar-collapse.sidebar-mini-md .sidebar-no-expand.main-sidebar:hover .nav-sidebar>.nav-item .nav-icon,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar>.nav-item .nav-icon,.sidebar-collapse.sidebar-mini-xs .sidebar-no-expand.main-sidebar:hover .nav-sidebar>.nav-item .nav-icon{margin-right:0}.nav-sidebar{position:relative}.nav-sidebar:hover{overflow:visible}.nav-sidebar>.nav-header,.sidebar-form{overflow:hidden;text-overflow:clip}.nav-sidebar .nav-item>.nav-link{position:relative}.nav-sidebar .nav-item>.nav-link>.float-right{margin-top:-7px;position:absolute;right:10px;top:50%}.main-sidebar .brand-text,.main-sidebar .logo-xl,.main-sidebar .logo-xs,.sidebar .nav-link p,.sidebar .user-panel .info{transition:margin-left .3s linear,opacity .3s ease,visibility .3s ease}@media (prefers-reduced-motion:reduce){.main-sidebar .brand-text,.main-sidebar .logo-xl,.main-sidebar .logo-xs,.sidebar .nav-link p,.sidebar .user-panel .info{transition:none}}html.control-sidebar-animate{overflow-x:hidden}.control-sidebar{bottom:calc(3.5rem + 1px);position:absolute;top:calc(3.5rem + 1px);z-index:1031}.control-sidebar,.control-sidebar::before{bottom:calc(3.5rem + 1px);display:none;right:-250px;width:250px;transition:right .3s ease-in-out,display .3s ease-in-out}@media (prefers-reduced-motion:reduce){.control-sidebar,.control-sidebar::before{transition:none}}.control-sidebar::before{content:"";display:block;position:fixed;top:0;z-index:-1}body.text-sm .control-sidebar{bottom:calc(2.9365rem + 1px);top:calc(2.93725rem + 1px)}.main-header.text-sm~.control-sidebar{top:calc(2.93725rem + 1px)}.main-footer.text-sm~.control-sidebar{bottom:calc(2.9365rem + 1px)}.control-sidebar-push-slide .content-wrapper,.control-sidebar-push-slide .main-footer{transition:margin-right .3s ease-in-out}@media (prefers-reduced-motion:reduce){.control-sidebar-push-slide .content-wrapper,.control-sidebar-push-slide .main-footer{transition:none}}.control-sidebar-open .control-sidebar{display:block}.control-sidebar-open .control-sidebar,.control-sidebar-open .control-sidebar::before{right:0}.control-sidebar-open.control-sidebar-push .content-wrapper,.control-sidebar-open.control-sidebar-push .main-footer,.control-sidebar-open.control-sidebar-push-slide .content-wrapper,.control-sidebar-open.control-sidebar-push-slide .main-footer{margin-right:250px}.control-sidebar-slide-open .control-sidebar{display:block}.control-sidebar-slide-open .control-sidebar,.control-sidebar-slide-open .control-sidebar::before{right:0;transition:right .3s ease-in-out,display .3s ease-in-out}@media (prefers-reduced-motion:reduce){.control-sidebar-slide-open .control-sidebar,.control-sidebar-slide-open .control-sidebar::before{transition:none}}.control-sidebar-slide-open.control-sidebar-push .content-wrapper,.control-sidebar-slide-open.control-sidebar-push .main-footer,.control-sidebar-slide-open.control-sidebar-push-slide .content-wrapper,.control-sidebar-slide-open.control-sidebar-push-slide .main-footer{margin-right:250px}.control-sidebar-dark{background-color:#343a40}.control-sidebar-dark,.control-sidebar-dark .nav-link,.control-sidebar-dark a{color:#c2c7d0}.control-sidebar-dark a:hover{color:#fff}.control-sidebar-dark h1,.control-sidebar-dark h2,.control-sidebar-dark h3,.control-sidebar-dark h4,.control-sidebar-dark h5,.control-sidebar-dark h6,.control-sidebar-dark label{color:#fff}.control-sidebar-dark .nav-tabs{background-color:rgba(255,255,255,.1);border-bottom:0;margin-bottom:5px}.control-sidebar-dark .nav-tabs .nav-item{margin:0}.control-sidebar-dark .nav-tabs .nav-link{border-radius:0;padding:10px 20px;position:relative;text-align:center}.control-sidebar-dark .nav-tabs .nav-link,.control-sidebar-dark .nav-tabs .nav-link.active,.control-sidebar-dark .nav-tabs .nav-link:active,.control-sidebar-dark .nav-tabs .nav-link:focus,.control-sidebar-dark .nav-tabs .nav-link:hover{border:0}.control-sidebar-dark .nav-tabs .nav-link.active,.control-sidebar-dark .nav-tabs .nav-link:active,.control-sidebar-dark .nav-tabs .nav-link:focus,.control-sidebar-dark .nav-tabs .nav-link:hover{border-bottom-color:transparent;border-left-color:transparent;border-top-color:transparent;color:#fff}.control-sidebar-dark .nav-tabs .nav-link.active{background-color:#343a40}.control-sidebar-dark .tab-pane{padding:10px 15px}.control-sidebar-light{color:#4b545c;background-color:#fff;border-left:1px solid #dee2e6}.text-sm .dropdown-menu{font-size:.875rem!important}.text-sm .dropdown-toggle::after{vertical-align:.2rem}.dropdown-item-title{font-size:1rem;margin:0}.dropdown-icon::after{margin-left:0}.dropdown-menu-lg{max-width:300px;min-width:280px;padding:0}.dropdown-menu-lg .dropdown-divider{margin:0}.dropdown-menu-lg .dropdown-item{padding:.5rem 1rem}.dropdown-menu-lg p{margin:0;white-space:normal}.dropdown-submenu{position:relative}.dropdown-submenu>a::after{border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid;float:right;margin-left:.5rem;margin-top:.5rem}.dropdown-submenu>.dropdown-menu{left:100%;margin-left:0;margin-top:0;top:0}.dropdown-hover .dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.nav-item.dropdown:hover>.dropdown-menu,.dropdown-hover:hover>.dropdown-menu{display:block}.dropdown-menu-xl{max-width:420px;min-width:360px;padding:0}.dropdown-menu-xl .dropdown-divider{margin:0}.dropdown-menu-xl .dropdown-item{padding:.5rem 1rem}.dropdown-menu-xl p{margin:0;white-space:normal}.dropdown-footer,.dropdown-header{display:block;font-size:.875rem;padding:.5rem 1rem;text-align:center}.open:not(.dropup)>.animated-dropdown-menu{-webkit-animation:flipInX .7s both;animation:flipInX .7s both;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:767.98px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background-color:#fff}}.navbar-nav>.user-menu>.nav-link::after{content:none}.navbar-nav>.user-menu>.dropdown-menu{border-top-left-radius:0;border-top-right-radius:0;padding:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom:1px solid #495057;border-top:1px solid #dee2e6;padding:15px}.navbar-nav>.user-menu>.dropdown-menu>.user-body::after{display:block;clear:both;content:""}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background-color:#fff!important;color:#495057!important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f8f9fa;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer::after{display:block;clear:both;content:""}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#6c757d}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f8f9fa}}.navbar-nav>.user-menu .user-image{border-radius:50%;float:left;height:2.1rem;margin-right:10px;margin-top:-2px;width:2.1rem}@media (min-width:576px){.navbar-nav>.user-menu .user-image{float:none;line-height:10px;margin-right:.4rem;margin-top:-8px}}.dark-mode .dropdown-menu{background-color:#343a40;color:#fff}.dark-mode .dropdown-item{color:#fff}.dark-mode .dropdown-item:focus,.dark-mode .dropdown-item:hover{background-color:#3f474e}.dark-mode .dropdown-divider{border-color:#6c757d}.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#3a4047;color:#fff}.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#fff}.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:focus,.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#3f474e;color:#dee2e6}.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:focus{background-color:#454d55}.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-body{border-color:#6c757d}.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-body a{background-color:transparent!important;color:#fff!important}.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-body a:focus,.dark-mode .navbar-nav>.user-menu>.dropdown-menu>.user-body a:hover{color:#ced4da!important}.nav-pills .nav-link{color:#6c757d}.nav-pills .nav-link:not(.active):hover{color:#007bff}.nav-pills .nav-item.dropdown.show .nav-link:hover{color:#fff}.nav-tabs.flex-column{border-bottom:0;border-right:1px solid #dee2e6}.nav-tabs.flex-column .nav-link{border-bottom-left-radius:.25rem;border-top-right-radius:0;margin-right:-1px}.nav-tabs.flex-column .nav-link:focus,.nav-tabs.flex-column .nav-link:hover{border-color:#e9ecef transparent #e9ecef #e9ecef}.nav-tabs.flex-column .nav-item.show .nav-link,.nav-tabs.flex-column .nav-link.active{border-color:#dee2e6 transparent #dee2e6 #dee2e6}.nav-tabs.flex-column.nav-tabs-right{border-left:1px solid #dee2e6;border-right:0}.nav-tabs.flex-column.nav-tabs-right .nav-link{border-bottom-left-radius:0;border-bottom-right-radius:.25rem;border-top-left-radius:0;border-top-right-radius:.25rem;margin-left:-1px}.nav-tabs.flex-column.nav-tabs-right .nav-link:focus,.nav-tabs.flex-column.nav-tabs-right .nav-link:hover{border-color:#e9ecef #e9ecef #e9ecef transparent}.nav-tabs.flex-column.nav-tabs-right .nav-item.show .nav-link,.nav-tabs.flex-column.nav-tabs-right .nav-link.active{border-color:#dee2e6 #dee2e6 #dee2e6 transparent}.navbar-no-expand{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-no-expand .nav-link{padding-left:1rem;padding-right:1rem}.navbar-no-expand .dropdown-menu{position:absolute}.navbar-light{background-color:#f8f9fa}.navbar-dark{background-color:#343a40;border-color:#4b545c}.navbar-primary{background-color:#007bff;color:#fff}.navbar-primary.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-primary.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-primary.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-primary.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-primary.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-primary.navbar-light .form-control-navbar,.navbar-primary.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#0071eb;border-color:#0065d1;color:rgba(52,58,64,.8)}.navbar-primary.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-primary.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-primary.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-primary.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-primary.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-primary.navbar-light .form-control-navbar:focus,.navbar-primary.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#006fe6;border-color:#0065d1!important;color:#343a40}.navbar-primary.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-primary.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-primary.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-primary.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-primary.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-primary.navbar-dark .form-control-navbar,.navbar-primary.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#1486ff;border-color:#2e93ff;color:rgba(255,255,255,.8)}.navbar-primary.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-primary.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-primary.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-primary.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-primary.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-primary.navbar-dark .form-control-navbar:focus,.navbar-primary.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#1a88ff;border-color:#2e93ff!important;color:#fff}.navbar-secondary{background-color:#6c757d;color:#fff}.navbar-secondary.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-secondary.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-secondary.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-secondary.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-secondary.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-secondary.navbar-light .form-control-navbar,.navbar-secondary.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#636b72;border-color:#575e64;color:rgba(52,58,64,.8)}.navbar-secondary.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-secondary.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-secondary.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-secondary.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-secondary.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-secondary.navbar-light .form-control-navbar:focus,.navbar-secondary.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#60686f;border-color:#575e64!important;color:#343a40}.navbar-secondary.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-secondary.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-secondary.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-secondary.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-secondary.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-secondary.navbar-dark .form-control-navbar,.navbar-secondary.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#757f88;border-color:#838c94;color:rgba(255,255,255,.8)}.navbar-secondary.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-secondary.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-secondary.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-secondary.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-secondary.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-secondary.navbar-dark .form-control-navbar:focus,.navbar-secondary.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#78828a;border-color:#838c94!important;color:#fff}.navbar-success{background-color:#28a745;color:#fff}.navbar-success.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-success.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-success.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-success.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-success.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-success.navbar-light .form-control-navbar,.navbar-success.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#24973e;border-color:#1f8236;color:rgba(52,58,64,.8)}.navbar-success.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-success.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-success.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-success.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-success.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-success.navbar-light .form-control-navbar:focus,.navbar-success.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#23923d;border-color:#1f8236!important;color:#343a40}.navbar-success.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-success.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-success.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-success.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-success.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-success.navbar-dark .form-control-navbar,.navbar-success.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#2cb74c;border-color:#31cc54;color:rgba(255,255,255,.8)}.navbar-success.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-success.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-success.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-success.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-success.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-success.navbar-dark .form-control-navbar:focus,.navbar-success.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#2dbc4e;border-color:#31cc54!important;color:#fff}.navbar-info{background-color:#17a2b8;color:#fff}.navbar-info.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-info.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-info.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-info.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-info.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-info.navbar-light .form-control-navbar,.navbar-info.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#1592a6;border-color:#127e8f;color:rgba(52,58,64,.8)}.navbar-info.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-info.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-info.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-info.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-info.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-info.navbar-light .form-control-navbar:focus,.navbar-info.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#148ea1;border-color:#127e8f!important;color:#343a40}.navbar-info.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-info.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-info.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-info.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-info.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-info.navbar-dark .form-control-navbar,.navbar-info.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#19b2ca;border-color:#1cc6e1;color:rgba(255,255,255,.8)}.navbar-info.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-info.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-info.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-info.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-info.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-info.navbar-dark .form-control-navbar:focus,.navbar-info.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#1ab6cf;border-color:#1cc6e1!important;color:#fff}.navbar-warning{background-color:#ffc107;color:#1f2d3d}.navbar-warning.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-warning.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-warning.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-warning.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-warning.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-warning.navbar-light .form-control-navbar,.navbar-warning.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f2b500;border-color:#d8a200;color:rgba(52,58,64,.8)}.navbar-warning.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-warning.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-warning.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-warning.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-warning.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-warning.navbar-light .form-control-navbar:focus,.navbar-warning.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#edb100;border-color:#d8a200!important;color:#343a40}.navbar-warning.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-warning.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-warning.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-warning.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-warning.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-warning.navbar-dark .form-control-navbar,.navbar-warning.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#ffc61b;border-color:#ffcc35;color:rgba(255,255,255,.8)}.navbar-warning.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-warning.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-warning.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-warning.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-warning.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-warning.navbar-dark .form-control-navbar:focus,.navbar-warning.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#ffc721;border-color:#ffcc35!important;color:#fff}.navbar-danger{background-color:#dc3545;color:#fff}.navbar-danger.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-danger.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-danger.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-danger.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-danger.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-danger.navbar-light .form-control-navbar,.navbar-danger.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#d72536;border-color:#c22231;color:rgba(52,58,64,.8)}.navbar-danger.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-danger.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-danger.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-danger.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-danger.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-danger.navbar-light .form-control-navbar:focus,.navbar-danger.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#d32535;border-color:#c22231!important;color:#343a40}.navbar-danger.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-danger.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-danger.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-danger.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-danger.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-danger.navbar-dark .form-control-navbar,.navbar-danger.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#df4655;border-color:#e35c69;color:rgba(255,255,255,.8)}.navbar-danger.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-danger.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-danger.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-danger.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-danger.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-danger.navbar-dark .form-control-navbar:focus,.navbar-danger.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e04b59;border-color:#e35c69!important;color:#fff}.navbar-lightblue{background-color:#3c8dbc;color:#fff}.navbar-lightblue.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-lightblue.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-lightblue.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-lightblue.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-lightblue.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-lightblue.navbar-light .form-control-navbar,.navbar-lightblue.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#3781ad;border-color:#317399;color:rgba(52,58,64,.8)}.navbar-lightblue.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-lightblue.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-lightblue.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-lightblue.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-lightblue.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-lightblue.navbar-light .form-control-navbar:focus,.navbar-lightblue.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#367fa9;border-color:#317399!important;color:#343a40}.navbar-lightblue.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-lightblue.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-lightblue.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-lightblue.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-lightblue.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-lightblue.navbar-dark .form-control-navbar,.navbar-lightblue.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#4897c5;border-color:#5ba2cb;color:rgba(255,255,255,.8)}.navbar-lightblue.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-lightblue.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-lightblue.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-lightblue.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-lightblue.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-lightblue.navbar-dark .form-control-navbar:focus,.navbar-lightblue.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#4c99c6;border-color:#5ba2cb!important;color:#fff}.navbar-navy{background-color:#001f3f;color:#fff}.navbar-navy.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-navy.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-navy.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-navy.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-navy.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-navy.navbar-light .form-control-navbar,.navbar-navy.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#00152b;border-color:#000811;color:rgba(52,58,64,.8)}.navbar-navy.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-navy.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-navy.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-navy.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-navy.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-navy.navbar-light .form-control-navbar:focus,.navbar-navy.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#001226;border-color:#000811!important;color:#343a40}.navbar-navy.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-navy.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-navy.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-navy.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-navy.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-navy.navbar-dark .form-control-navbar,.navbar-navy.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#002953;border-color:#00366d;color:rgba(255,255,255,.8)}.navbar-navy.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-navy.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-navy.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-navy.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-navy.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-navy.navbar-dark .form-control-navbar:focus,.navbar-navy.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#002c59;border-color:#00366d!important;color:#fff}.navbar-olive{background-color:#3d9970;color:#fff}.navbar-olive.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-olive.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-olive.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-olive.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-olive.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-olive.navbar-light .form-control-navbar,.navbar-olive.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#378a65;border-color:#307858;color:rgba(52,58,64,.8)}.navbar-olive.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-olive.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-olive.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-olive.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-olive.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-olive.navbar-light .form-control-navbar:focus,.navbar-olive.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#368763;border-color:#307858!important;color:#343a40}.navbar-olive.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-olive.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-olive.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-olive.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-olive.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-olive.navbar-dark .form-control-navbar,.navbar-olive.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#43a87b;border-color:#4cb888;color:rgba(255,255,255,.8)}.navbar-olive.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-olive.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-olive.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-olive.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-olive.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-olive.navbar-dark .form-control-navbar:focus,.navbar-olive.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#44ab7d;border-color:#4cb888!important;color:#fff}.navbar-lime{background-color:#01ff70;color:#1f2d3d}.navbar-lime.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-lime.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-lime.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-lime.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-lime.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-lime.navbar-light .form-control-navbar,.navbar-lime.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#00ec67;border-color:#00d25c;color:rgba(52,58,64,.8)}.navbar-lime.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-lime.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-lime.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-lime.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-lime.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-lime.navbar-light .form-control-navbar:focus,.navbar-lime.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#00e765;border-color:#00d25c!important;color:#343a40}.navbar-lime.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-lime.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-lime.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-lime.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-lime.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-lime.navbar-dark .form-control-navbar,.navbar-lime.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#15ff7b;border-color:#2fff8a;color:rgba(255,255,255,.8)}.navbar-lime.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-lime.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-lime.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-lime.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-lime.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-lime.navbar-dark .form-control-navbar:focus,.navbar-lime.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#1bff7e;border-color:#2fff8a!important;color:#fff}.navbar-fuchsia{background-color:#f012be;color:#fff}.navbar-fuchsia.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-fuchsia.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-fuchsia.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-fuchsia.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-fuchsia.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-fuchsia.navbar-light .form-control-navbar,.navbar-fuchsia.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#df0eb0;border-color:#c70d9d;color:rgba(52,58,64,.8)}.navbar-fuchsia.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-fuchsia.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-fuchsia.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-fuchsia.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-fuchsia.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-fuchsia.navbar-light .form-control-navbar:focus,.navbar-fuchsia.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#db0ead;border-color:#c70d9d!important;color:#343a40}.navbar-fuchsia.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-fuchsia.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-fuchsia.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-fuchsia.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-fuchsia.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-fuchsia.navbar-dark .form-control-navbar,.navbar-fuchsia.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f125c3;border-color:#f33dca;color:rgba(255,255,255,.8)}.navbar-fuchsia.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-fuchsia.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-fuchsia.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-fuchsia.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-fuchsia.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-fuchsia.navbar-dark .form-control-navbar:focus,.navbar-fuchsia.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f22ac5;border-color:#f33dca!important;color:#fff}.navbar-maroon{background-color:#d81b60;color:#fff}.navbar-maroon.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-maroon.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-maroon.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-maroon.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-maroon.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-maroon.navbar-light .form-control-navbar,.navbar-maroon.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#c61958;border-color:#af164e;color:rgba(52,58,64,.8)}.navbar-maroon.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-maroon.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-maroon.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-maroon.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-maroon.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-maroon.navbar-light .form-control-navbar:focus,.navbar-maroon.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#c11856;border-color:#af164e!important;color:#343a40}.navbar-maroon.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-maroon.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-maroon.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-maroon.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-maroon.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-maroon.navbar-dark .form-control-navbar,.navbar-maroon.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e4246a;border-color:#e63a79;color:rgba(255,255,255,.8)}.navbar-maroon.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-maroon.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-maroon.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-maroon.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-maroon.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-maroon.navbar-dark .form-control-navbar:focus,.navbar-maroon.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e4286d;border-color:#e63a79!important;color:#fff}.navbar-blue{background-color:#007bff;color:#fff}.navbar-blue.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-blue.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-blue.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-blue.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-blue.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-blue.navbar-light .form-control-navbar,.navbar-blue.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#0071eb;border-color:#0065d1;color:rgba(52,58,64,.8)}.navbar-blue.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-blue.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-blue.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-blue.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-blue.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-blue.navbar-light .form-control-navbar:focus,.navbar-blue.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#006fe6;border-color:#0065d1!important;color:#343a40}.navbar-blue.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-blue.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-blue.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-blue.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-blue.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-blue.navbar-dark .form-control-navbar,.navbar-blue.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#1486ff;border-color:#2e93ff;color:rgba(255,255,255,.8)}.navbar-blue.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-blue.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-blue.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-blue.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-blue.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-blue.navbar-dark .form-control-navbar:focus,.navbar-blue.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#1a88ff;border-color:#2e93ff!important;color:#fff}.navbar-indigo{background-color:#6610f2;color:#fff}.navbar-indigo.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-indigo.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-indigo.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-indigo.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-indigo.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-indigo.navbar-light .form-control-navbar,.navbar-indigo.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#5d0ce1;border-color:#530bc9;color:rgba(52,58,64,.8)}.navbar-indigo.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-indigo.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-indigo.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-indigo.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-indigo.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-indigo.navbar-light .form-control-navbar:focus,.navbar-indigo.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#5b0cdd;border-color:#530bc9!important;color:#343a40}.navbar-indigo.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-indigo.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-indigo.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-indigo.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-indigo.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-indigo.navbar-dark .form-control-navbar,.navbar-indigo.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#7223f3;border-color:#823cf4;color:rgba(255,255,255,.8)}.navbar-indigo.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-indigo.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-indigo.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-indigo.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-indigo.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-indigo.navbar-dark .form-control-navbar:focus,.navbar-indigo.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#7528f3;border-color:#823cf4!important;color:#fff}.navbar-purple{background-color:#6f42c1;color:#fff}.navbar-purple.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-purple.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-purple.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-purple.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-purple.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-purple.navbar-light .form-control-navbar,.navbar-purple.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#663bb4;border-color:#5b35a0;color:rgba(52,58,64,.8)}.navbar-purple.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-purple.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-purple.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-purple.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-purple.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-purple.navbar-light .form-control-navbar:focus,.navbar-purple.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#643ab0;border-color:#5b35a0!important;color:#343a40}.navbar-purple.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-purple.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-purple.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-purple.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-purple.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-purple.navbar-dark .form-control-navbar,.navbar-purple.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#7b51c6;border-color:#8965cc;color:rgba(255,255,255,.8)}.navbar-purple.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-purple.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-purple.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-purple.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-purple.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-purple.navbar-dark .form-control-navbar:focus,.navbar-purple.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#7e55c7;border-color:#8965cc!important;color:#fff}.navbar-pink{background-color:#e83e8c;color:#fff}.navbar-pink.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-pink.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-pink.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-pink.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-pink.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-pink.navbar-light .form-control-navbar,.navbar-pink.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e62c81;border-color:#de1a74;color:rgba(52,58,64,.8)}.navbar-pink.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-pink.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-pink.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-pink.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-pink.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-pink.navbar-light .form-control-navbar:focus,.navbar-pink.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e5277e;border-color:#de1a74!important;color:#343a40}.navbar-pink.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-pink.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-pink.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-pink.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-pink.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-pink.navbar-dark .form-control-navbar,.navbar-pink.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#ea5097;border-color:#ed67a4;color:rgba(255,255,255,.8)}.navbar-pink.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-pink.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-pink.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-pink.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-pink.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-pink.navbar-dark .form-control-navbar:focus,.navbar-pink.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#eb559a;border-color:#ed67a4!important;color:#fff}.navbar-red{background-color:#dc3545;color:#fff}.navbar-red.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-red.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-red.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-red.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-red.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-red.navbar-light .form-control-navbar,.navbar-red.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#d72536;border-color:#c22231;color:rgba(52,58,64,.8)}.navbar-red.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-red.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-red.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-red.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-red.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-red.navbar-light .form-control-navbar:focus,.navbar-red.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#d32535;border-color:#c22231!important;color:#343a40}.navbar-red.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-red.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-red.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-red.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-red.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-red.navbar-dark .form-control-navbar,.navbar-red.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#df4655;border-color:#e35c69;color:rgba(255,255,255,.8)}.navbar-red.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-red.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-red.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-red.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-red.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-red.navbar-dark .form-control-navbar:focus,.navbar-red.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e04b59;border-color:#e35c69!important;color:#fff}.navbar-orange{background-color:#fd7e14;color:#1f2d3d}.navbar-orange.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-orange.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-orange.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-orange.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-orange.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-orange.navbar-light .form-control-navbar,.navbar-orange.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#fa7302;border-color:#e16702;color:rgba(52,58,64,.8)}.navbar-orange.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-orange.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-orange.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-orange.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-orange.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-orange.navbar-light .form-control-navbar:focus,.navbar-orange.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f57102;border-color:#e16702!important;color:#343a40}.navbar-orange.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-orange.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-orange.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-orange.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-orange.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-orange.navbar-dark .form-control-navbar,.navbar-orange.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#fd8928;border-color:#fd9742;color:rgba(255,255,255,.8)}.navbar-orange.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-orange.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-orange.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-orange.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-orange.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-orange.navbar-dark .form-control-navbar:focus,.navbar-orange.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#fd8c2d;border-color:#fd9742!important;color:#fff}.navbar-yellow{background-color:#ffc107;color:#1f2d3d}.navbar-yellow.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-yellow.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-yellow.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-yellow.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-yellow.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-yellow.navbar-light .form-control-navbar,.navbar-yellow.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f2b500;border-color:#d8a200;color:rgba(52,58,64,.8)}.navbar-yellow.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-yellow.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-yellow.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-yellow.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-yellow.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-yellow.navbar-light .form-control-navbar:focus,.navbar-yellow.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#edb100;border-color:#d8a200!important;color:#343a40}.navbar-yellow.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-yellow.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-yellow.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-yellow.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-yellow.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-yellow.navbar-dark .form-control-navbar,.navbar-yellow.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#ffc61b;border-color:#ffcc35;color:rgba(255,255,255,.8)}.navbar-yellow.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-yellow.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-yellow.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-yellow.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-yellow.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-yellow.navbar-dark .form-control-navbar:focus,.navbar-yellow.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#ffc721;border-color:#ffcc35!important;color:#fff}.navbar-green{background-color:#28a745;color:#fff}.navbar-green.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-green.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-green.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-green.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-green.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-green.navbar-light .form-control-navbar,.navbar-green.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#24973e;border-color:#1f8236;color:rgba(52,58,64,.8)}.navbar-green.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-green.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-green.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-green.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-green.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-green.navbar-light .form-control-navbar:focus,.navbar-green.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#23923d;border-color:#1f8236!important;color:#343a40}.navbar-green.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-green.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-green.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-green.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-green.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-green.navbar-dark .form-control-navbar,.navbar-green.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#2cb74c;border-color:#31cc54;color:rgba(255,255,255,.8)}.navbar-green.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-green.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-green.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-green.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-green.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-green.navbar-dark .form-control-navbar:focus,.navbar-green.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#2dbc4e;border-color:#31cc54!important;color:#fff}.navbar-teal{background-color:#20c997;color:#fff}.navbar-teal.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-teal.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-teal.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-teal.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-teal.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-teal.navbar-light .form-control-navbar,.navbar-teal.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#1db78a;border-color:#1aa179;color:rgba(52,58,64,.8)}.navbar-teal.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-teal.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-teal.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-teal.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-teal.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-teal.navbar-light .form-control-navbar:focus,.navbar-teal.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#1cb386;border-color:#1aa179!important;color:#343a40}.navbar-teal.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-teal.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-teal.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-teal.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-teal.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-teal.navbar-dark .form-control-navbar,.navbar-teal.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#23dba4;border-color:#38dfae;color:rgba(255,255,255,.8)}.navbar-teal.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-teal.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-teal.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-teal.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-teal.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-teal.navbar-dark .form-control-navbar:focus,.navbar-teal.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#26dca6;border-color:#38dfae!important;color:#fff}.navbar-cyan{background-color:#17a2b8;color:#fff}.navbar-cyan.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-cyan.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-cyan.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-cyan.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-cyan.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-cyan.navbar-light .form-control-navbar,.navbar-cyan.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#1592a6;border-color:#127e8f;color:rgba(52,58,64,.8)}.navbar-cyan.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-cyan.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-cyan.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-cyan.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-cyan.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-cyan.navbar-light .form-control-navbar:focus,.navbar-cyan.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#148ea1;border-color:#127e8f!important;color:#343a40}.navbar-cyan.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-cyan.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-cyan.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-cyan.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-cyan.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-cyan.navbar-dark .form-control-navbar,.navbar-cyan.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#19b2ca;border-color:#1cc6e1;color:rgba(255,255,255,.8)}.navbar-cyan.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-cyan.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-cyan.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-cyan.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-cyan.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-cyan.navbar-dark .form-control-navbar:focus,.navbar-cyan.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#1ab6cf;border-color:#1cc6e1!important;color:#fff}.navbar-white{background-color:#fff;color:#1f2d3d}.navbar-white.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-white.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-white.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-white.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-white.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-white.navbar-light .form-control-navbar,.navbar-white.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f5f5f5;border-color:#e8e8e8;color:rgba(52,58,64,.8)}.navbar-white.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-white.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-white.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-white.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-white.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-white.navbar-light .form-control-navbar:focus,.navbar-white.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f2f2f2;border-color:#e8e8e8!important;color:#343a40}.navbar-white.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-white.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-white.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-white.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-white.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-white.navbar-dark .form-control-navbar,.navbar-white.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#fff;border-color:#fff;color:rgba(255,255,255,.8)}.navbar-white.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-white.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-white.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-white.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-white.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-white.navbar-dark .form-control-navbar:focus,.navbar-white.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#fff;border-color:#fff!important;color:#fff}.navbar-gray{background-color:#6c757d;color:#fff}.navbar-gray.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-gray.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-gray.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-gray.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-gray.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-gray.navbar-light .form-control-navbar,.navbar-gray.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#636b72;border-color:#575e64;color:rgba(52,58,64,.8)}.navbar-gray.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-gray.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-gray.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-gray.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-gray.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-gray.navbar-light .form-control-navbar:focus,.navbar-gray.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#60686f;border-color:#575e64!important;color:#343a40}.navbar-gray.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-gray.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-gray.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-gray.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-gray.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-gray.navbar-dark .form-control-navbar,.navbar-gray.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#757f88;border-color:#838c94;color:rgba(255,255,255,.8)}.navbar-gray.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-gray.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-gray.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-gray.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-gray.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-gray.navbar-dark .form-control-navbar:focus,.navbar-gray.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#78828a;border-color:#838c94!important;color:#fff}.navbar-gray-dark{background-color:#343a40;color:#fff}.navbar-gray-dark.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.navbar-gray-dark.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.navbar-gray-dark.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-gray-dark.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.navbar-gray-dark.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.navbar-gray-dark.navbar-light .form-control-navbar,.navbar-gray-dark.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#2b3035;border-color:#1f2327;color:rgba(52,58,64,.8)}.navbar-gray-dark.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.navbar-gray-dark.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.navbar-gray-dark.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.navbar-gray-dark.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.navbar-gray-dark.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.navbar-gray-dark.navbar-light .form-control-navbar:focus,.navbar-gray-dark.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#292d32;border-color:#1f2327!important;color:#343a40}.navbar-gray-dark.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.navbar-gray-dark.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.navbar-gray-dark.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-gray-dark.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.navbar-gray-dark.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.navbar-gray-dark.navbar-dark .form-control-navbar,.navbar-gray-dark.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#3d444b;border-color:#495159;color:rgba(255,255,255,.8)}.navbar-gray-dark.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.navbar-gray-dark.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.navbar-gray-dark.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.navbar-gray-dark.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.navbar-gray-dark.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.navbar-gray-dark.navbar-dark .form-control-navbar:focus,.navbar-gray-dark.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#3f474e;border-color:#495159!important;color:#fff}.dark-mode .nav-pills .nav-link{color:#ced4da}.dark-mode .nav-tabs{border-color:#56606a}.dark-mode .nav-tabs .nav-link:focus,.dark-mode .nav-tabs .nav-link:hover{border-color:#56606a}.dark-mode .nav-tabs .nav-item.show .nav-link,.dark-mode .nav-tabs .nav-link.active{background-color:#343a40;border-color:#56606a #56606a transparent #56606a;color:#fff}.dark-mode .nav-tabs.flex-column .nav-item.show .nav-link.active,.dark-mode .nav-tabs.flex-column .nav-item.show .nav-link:focus,.dark-mode .nav-tabs.flex-column .nav-item.show .nav-link:hover,.dark-mode .nav-tabs.flex-column .nav-link.active,.dark-mode .nav-tabs.flex-column .nav-link:focus,.dark-mode .nav-tabs.flex-column .nav-link:hover{border-color:#56606a transparent #56606a #56606a}.dark-mode .nav-tabs.flex-column .nav-item.show .nav-link:focus,.dark-mode .nav-tabs.flex-column .nav-item.show .nav-link:hover,.dark-mode .nav-tabs.flex-column .nav-link:focus,.dark-mode .nav-tabs.flex-column .nav-link:hover{background-color:#3f474e}.dark-mode .nav-tabs.flex-column.nav-tabs-right{border-color:#56606a}.dark-mode .nav-tabs.flex-column.nav-tabs-right .nav-link.active,.dark-mode .nav-tabs.flex-column.nav-tabs-right .nav-link:focus,.dark-mode .nav-tabs.flex-column.nav-tabs-right .nav-link:hover{border-color:#56606a #56606a #56606a transparent}.dark-mode .navbar-light{background-color:#f8f9fa}.dark-mode .navbar-dark{background-color:#343a40;border-color:#4b545c}.dark-mode .navbar-primary{background-color:#3f6791;color:#fff}.dark-mode .navbar-primary.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-primary.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-primary.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-primary.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-primary.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-primary.navbar-light .form-control-navbar,.dark-mode .navbar-primary.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#395d83;border-color:#315071;color:rgba(52,58,64,.8)}.dark-mode .navbar-primary.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-primary.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-primary.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-primary.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-primary.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-primary.navbar-light .form-control-navbar:focus,.dark-mode .navbar-primary.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#375a7f;border-color:#315071!important;color:#343a40}.dark-mode .navbar-primary.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-primary.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-primary.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-primary.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-primary.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-primary.navbar-dark .form-control-navbar,.dark-mode .navbar-primary.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#45719f;border-color:#4d7eb1;color:rgba(255,255,255,.8)}.dark-mode .navbar-primary.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-primary.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-primary.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-primary.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-primary.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-primary.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-primary.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#4774a3;border-color:#4d7eb1!important;color:#fff}.dark-mode .navbar-secondary{background-color:#6c757d;color:#fff}.dark-mode .navbar-secondary.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-secondary.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-secondary.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-secondary.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-secondary.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-secondary.navbar-light .form-control-navbar,.dark-mode .navbar-secondary.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#636b72;border-color:#575e64;color:rgba(52,58,64,.8)}.dark-mode .navbar-secondary.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-secondary.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-secondary.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-secondary.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-secondary.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-secondary.navbar-light .form-control-navbar:focus,.dark-mode .navbar-secondary.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#60686f;border-color:#575e64!important;color:#343a40}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar,.dark-mode .navbar-secondary.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#757f88;border-color:#838c94;color:rgba(255,255,255,.8)}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-secondary.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#78828a;border-color:#838c94!important;color:#fff}.dark-mode .navbar-success{background-color:#00bc8c;color:#fff}.dark-mode .navbar-success.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-success.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-success.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-success.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-success.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-success.navbar-light .form-control-navbar,.dark-mode .navbar-success.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#00a87d;border-color:#008e6a;color:rgba(52,58,64,.8)}.dark-mode .navbar-success.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-success.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-success.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-success.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-success.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-success.navbar-light .form-control-navbar:focus,.dark-mode .navbar-success.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#00a379;border-color:#008e6a!important;color:#343a40}.dark-mode .navbar-success.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-success.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-success.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-success.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-success.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-success.navbar-dark .form-control-navbar,.dark-mode .navbar-success.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#00d09b;border-color:#00eaae;color:rgba(255,255,255,.8)}.dark-mode .navbar-success.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-success.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-success.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-success.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-success.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-success.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-success.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#00d69f;border-color:#00eaae!important;color:#fff}.dark-mode .navbar-info{background-color:#3498db;color:#fff}.dark-mode .navbar-info.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-info.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-info.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-info.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-info.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-info.navbar-light .form-control-navbar,.dark-mode .navbar-info.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#268fd5;border-color:#2280bf;color:rgba(52,58,64,.8)}.dark-mode .navbar-info.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-info.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-info.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-info.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-info.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-info.navbar-light .form-control-navbar:focus,.dark-mode .navbar-info.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#258cd1;border-color:#2280bf!important;color:#343a40}.dark-mode .navbar-info.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-info.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-info.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-info.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-info.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-info.navbar-dark .form-control-navbar,.dark-mode .navbar-info.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#45a1de;border-color:#5bace2;color:rgba(255,255,255,.8)}.dark-mode .navbar-info.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-info.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-info.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-info.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-info.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-info.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-info.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#4aa3df;border-color:#5bace2!important;color:#fff}.dark-mode .navbar-warning{background-color:#f39c12;color:#1f2d3d}.dark-mode .navbar-warning.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-warning.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-warning.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-warning.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-warning.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-warning.navbar-light .form-control-navbar,.dark-mode .navbar-warning.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e5910c;border-color:#cd820a;color:rgba(52,58,64,.8)}.dark-mode .navbar-warning.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-warning.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-warning.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-warning.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-warning.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-warning.navbar-light .form-control-navbar:focus,.dark-mode .navbar-warning.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e08e0b;border-color:#cd820a!important;color:#343a40}.dark-mode .navbar-warning.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-warning.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-warning.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-warning.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-warning.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-warning.navbar-dark .form-control-navbar,.dark-mode .navbar-warning.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f4a425;border-color:#f5ae3e;color:rgba(255,255,255,.8)}.dark-mode .navbar-warning.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-warning.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-warning.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-warning.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-warning.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-warning.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-warning.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f4a62a;border-color:#f5ae3e!important;color:#fff}.dark-mode .navbar-danger{background-color:#e74c3c;color:#fff}.dark-mode .navbar-danger.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-danger.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-danger.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-danger.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-danger.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-danger.navbar-light .form-control-navbar,.dark-mode .navbar-danger.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e53b2a;border-color:#da2d1b;color:rgba(52,58,64,.8)}.dark-mode .navbar-danger.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-danger.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-danger.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-danger.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-danger.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-danger.navbar-light .form-control-navbar:focus,.dark-mode .navbar-danger.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e43725;border-color:#da2d1b!important;color:#343a40}.dark-mode .navbar-danger.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-danger.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-danger.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-danger.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-danger.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-danger.navbar-dark .form-control-navbar,.dark-mode .navbar-danger.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e95d4e;border-color:#ec7265;color:rgba(255,255,255,.8)}.dark-mode .navbar-danger.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-danger.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-danger.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-danger.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-danger.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-danger.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-danger.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#ea6153;border-color:#ec7265!important;color:#fff}.dark-mode .navbar-lightblue{background-color:#86bad8;color:#1f2d3d}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar,.dark-mode .navbar-lightblue.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#76b1d3;border-color:#63a6cd;color:rgba(52,58,64,.8)}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:focus,.dark-mode .navbar-lightblue.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#72afd2;border-color:#63a6cd!important;color:#343a40}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar,.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#95c3dd;border-color:#a9cee3;color:rgba(255,255,255,.8)}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-lightblue.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#99c5de;border-color:#a9cee3!important;color:#fff}.dark-mode .navbar-navy{background-color:#002c59;color:#fff}.dark-mode .navbar-navy.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-navy.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-navy.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-navy.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-navy.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-navy.navbar-light .form-control-navbar,.dark-mode .navbar-navy.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#024;border-color:#00152b;color:rgba(52,58,64,.8)}.dark-mode .navbar-navy.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-navy.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-navy.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-navy.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-navy.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-navy.navbar-light .form-control-navbar:focus,.dark-mode .navbar-navy.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#001f3f;border-color:#00152b!important;color:#343a40}.dark-mode .navbar-navy.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-navy.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-navy.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-navy.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-navy.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-navy.navbar-dark .form-control-navbar,.dark-mode .navbar-navy.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#00366d;border-color:#004286;color:rgba(255,255,255,.8)}.dark-mode .navbar-navy.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-navy.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-navy.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-navy.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-navy.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-navy.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-navy.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#003872;border-color:#004286!important;color:#fff}.dark-mode .navbar-olive{background-color:#74c8a3;color:#1f2d3d}.dark-mode .navbar-olive.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-olive.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-olive.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-olive.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-olive.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-olive.navbar-light .form-control-navbar,.dark-mode .navbar-olive.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#66c299;border-color:#53bb8d;color:rgba(52,58,64,.8)}.dark-mode .navbar-olive.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-olive.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-olive.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-olive.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-olive.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-olive.navbar-light .form-control-navbar:focus,.dark-mode .navbar-olive.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#62c096;border-color:#53bb8d!important;color:#343a40}.dark-mode .navbar-olive.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-olive.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-olive.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-olive.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-olive.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-olive.navbar-dark .form-control-navbar,.dark-mode .navbar-olive.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#83ceac;border-color:#95d5b8;color:rgba(255,255,255,.8)}.dark-mode .navbar-olive.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-olive.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-olive.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-olive.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-olive.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-olive.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-olive.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#87cfaf;border-color:#95d5b8!important;color:#fff}.dark-mode .navbar-lime{background-color:#67ffa9;color:#1f2d3d}.dark-mode .navbar-lime.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lime.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lime.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lime.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lime.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-lime.navbar-light .form-control-navbar,.dark-mode .navbar-lime.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#53ff9e;border-color:#39ff90;color:rgba(52,58,64,.8)}.dark-mode .navbar-lime.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-lime.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-lime.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-lime.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-lime.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-lime.navbar-light .form-control-navbar:focus,.dark-mode .navbar-lime.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#4eff9b;border-color:#39ff90!important;color:#343a40}.dark-mode .navbar-lime.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lime.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lime.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lime.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lime.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-lime.navbar-dark .form-control-navbar,.dark-mode .navbar-lime.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#7bffb5;border-color:#95ffc3;color:rgba(255,255,255,.8)}.dark-mode .navbar-lime.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-lime.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-lime.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-lime.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-lime.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-lime.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-lime.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#81ffb8;border-color:#95ffc3!important;color:#fff}.dark-mode .navbar-fuchsia{background-color:#f672d8;color:#1f2d3d}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar,.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f55fd3;border-color:#f347cc;color:rgba(52,58,64,.8)}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:focus,.dark-mode .navbar-fuchsia.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f55ad2;border-color:#f347cc!important;color:#343a40}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar,.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f785de;border-color:#f99de4;color:rgba(255,255,255,.8)}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-fuchsia.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f88adf;border-color:#f99de4!important;color:#fff}.dark-mode .navbar-maroon{background-color:#ed6c9b;color:#1f2d3d}.dark-mode .navbar-maroon.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-maroon.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-maroon.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-maroon.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-maroon.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-maroon.navbar-light .form-control-navbar,.dark-mode .navbar-maroon.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#ea5a8f;border-color:#e8447f;color:rgba(52,58,64,.8)}.dark-mode .navbar-maroon.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-maroon.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-maroon.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-maroon.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-maroon.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-maroon.navbar-light .form-control-navbar:focus,.dark-mode .navbar-maroon.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#ea568c;border-color:#e8447f!important;color:#343a40}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar,.dark-mode .navbar-maroon.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#ef7ea8;border-color:#f295b7;color:rgba(255,255,255,.8)}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-maroon.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f083ab;border-color:#f295b7!important;color:#fff}.dark-mode .navbar-blue{background-color:#3f6791;color:#fff}.dark-mode .navbar-blue.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-blue.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-blue.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-blue.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-blue.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-blue.navbar-light .form-control-navbar,.dark-mode .navbar-blue.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#395d83;border-color:#315071;color:rgba(52,58,64,.8)}.dark-mode .navbar-blue.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-blue.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-blue.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-blue.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-blue.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-blue.navbar-light .form-control-navbar:focus,.dark-mode .navbar-blue.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#375a7f;border-color:#315071!important;color:#343a40}.dark-mode .navbar-blue.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-blue.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-blue.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-blue.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-blue.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-blue.navbar-dark .form-control-navbar,.dark-mode .navbar-blue.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#45719f;border-color:#4d7eb1;color:rgba(255,255,255,.8)}.dark-mode .navbar-blue.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-blue.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-blue.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-blue.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-blue.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-blue.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-blue.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#4774a3;border-color:#4d7eb1!important;color:#fff}.dark-mode .navbar-indigo{background-color:#6610f2;color:#fff}.dark-mode .navbar-indigo.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-indigo.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-indigo.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-indigo.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-indigo.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-indigo.navbar-light .form-control-navbar,.dark-mode .navbar-indigo.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#5d0ce1;border-color:#530bc9;color:rgba(52,58,64,.8)}.dark-mode .navbar-indigo.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-indigo.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-indigo.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-indigo.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-indigo.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-indigo.navbar-light .form-control-navbar:focus,.dark-mode .navbar-indigo.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#5b0cdd;border-color:#530bc9!important;color:#343a40}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar,.dark-mode .navbar-indigo.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#7223f3;border-color:#823cf4;color:rgba(255,255,255,.8)}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-indigo.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#7528f3;border-color:#823cf4!important;color:#fff}.dark-mode .navbar-purple{background-color:#6f42c1;color:#fff}.dark-mode .navbar-purple.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-purple.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-purple.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-purple.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-purple.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-purple.navbar-light .form-control-navbar,.dark-mode .navbar-purple.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#663bb4;border-color:#5b35a0;color:rgba(52,58,64,.8)}.dark-mode .navbar-purple.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-purple.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-purple.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-purple.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-purple.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-purple.navbar-light .form-control-navbar:focus,.dark-mode .navbar-purple.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#643ab0;border-color:#5b35a0!important;color:#343a40}.dark-mode .navbar-purple.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-purple.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-purple.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-purple.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-purple.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-purple.navbar-dark .form-control-navbar,.dark-mode .navbar-purple.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#7b51c6;border-color:#8965cc;color:rgba(255,255,255,.8)}.dark-mode .navbar-purple.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-purple.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-purple.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-purple.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-purple.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-purple.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-purple.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#7e55c7;border-color:#8965cc!important;color:#fff}.dark-mode .navbar-pink{background-color:#e83e8c;color:#fff}.dark-mode .navbar-pink.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-pink.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-pink.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-pink.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-pink.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-pink.navbar-light .form-control-navbar,.dark-mode .navbar-pink.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e62c81;border-color:#de1a74;color:rgba(52,58,64,.8)}.dark-mode .navbar-pink.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-pink.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-pink.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-pink.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-pink.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-pink.navbar-light .form-control-navbar:focus,.dark-mode .navbar-pink.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e5277e;border-color:#de1a74!important;color:#343a40}.dark-mode .navbar-pink.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-pink.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-pink.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-pink.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-pink.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-pink.navbar-dark .form-control-navbar,.dark-mode .navbar-pink.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#ea5097;border-color:#ed67a4;color:rgba(255,255,255,.8)}.dark-mode .navbar-pink.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-pink.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-pink.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-pink.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-pink.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-pink.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-pink.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#eb559a;border-color:#ed67a4!important;color:#fff}.dark-mode .navbar-red{background-color:#e74c3c;color:#fff}.dark-mode .navbar-red.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-red.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-red.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-red.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-red.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-red.navbar-light .form-control-navbar,.dark-mode .navbar-red.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e53b2a;border-color:#da2d1b;color:rgba(52,58,64,.8)}.dark-mode .navbar-red.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-red.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-red.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-red.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-red.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-red.navbar-light .form-control-navbar:focus,.dark-mode .navbar-red.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e43725;border-color:#da2d1b!important;color:#343a40}.dark-mode .navbar-red.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-red.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-red.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-red.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-red.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-red.navbar-dark .form-control-navbar,.dark-mode .navbar-red.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e95d4e;border-color:#ec7265;color:rgba(255,255,255,.8)}.dark-mode .navbar-red.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-red.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-red.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-red.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-red.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-red.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-red.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#ea6153;border-color:#ec7265!important;color:#fff}.dark-mode .navbar-orange{background-color:#fd7e14;color:#1f2d3d}.dark-mode .navbar-orange.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-orange.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-orange.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-orange.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-orange.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-orange.navbar-light .form-control-navbar,.dark-mode .navbar-orange.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#fa7302;border-color:#e16702;color:rgba(52,58,64,.8)}.dark-mode .navbar-orange.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-orange.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-orange.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-orange.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-orange.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-orange.navbar-light .form-control-navbar:focus,.dark-mode .navbar-orange.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f57102;border-color:#e16702!important;color:#343a40}.dark-mode .navbar-orange.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-orange.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-orange.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-orange.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-orange.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-orange.navbar-dark .form-control-navbar,.dark-mode .navbar-orange.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#fd8928;border-color:#fd9742;color:rgba(255,255,255,.8)}.dark-mode .navbar-orange.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-orange.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-orange.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-orange.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-orange.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-orange.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-orange.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#fd8c2d;border-color:#fd9742!important;color:#fff}.dark-mode .navbar-yellow{background-color:#f39c12;color:#1f2d3d}.dark-mode .navbar-yellow.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-yellow.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-yellow.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-yellow.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-yellow.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-yellow.navbar-light .form-control-navbar,.dark-mode .navbar-yellow.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#e5910c;border-color:#cd820a;color:rgba(52,58,64,.8)}.dark-mode .navbar-yellow.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-yellow.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-yellow.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-yellow.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-yellow.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-yellow.navbar-light .form-control-navbar:focus,.dark-mode .navbar-yellow.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e08e0b;border-color:#cd820a!important;color:#343a40}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar,.dark-mode .navbar-yellow.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f4a425;border-color:#f5ae3e;color:rgba(255,255,255,.8)}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-yellow.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f4a62a;border-color:#f5ae3e!important;color:#fff}.dark-mode .navbar-green{background-color:#00bc8c;color:#fff}.dark-mode .navbar-green.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-green.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-green.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-green.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-green.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-green.navbar-light .form-control-navbar,.dark-mode .navbar-green.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#00a87d;border-color:#008e6a;color:rgba(52,58,64,.8)}.dark-mode .navbar-green.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-green.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-green.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-green.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-green.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-green.navbar-light .form-control-navbar:focus,.dark-mode .navbar-green.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#00a379;border-color:#008e6a!important;color:#343a40}.dark-mode .navbar-green.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-green.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-green.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-green.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-green.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-green.navbar-dark .form-control-navbar,.dark-mode .navbar-green.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#00d09b;border-color:#00eaae;color:rgba(255,255,255,.8)}.dark-mode .navbar-green.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-green.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-green.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-green.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-green.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-green.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-green.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#00d69f;border-color:#00eaae!important;color:#fff}.dark-mode .navbar-teal{background-color:#20c997;color:#fff}.dark-mode .navbar-teal.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-teal.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-teal.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-teal.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-teal.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-teal.navbar-light .form-control-navbar,.dark-mode .navbar-teal.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#1db78a;border-color:#1aa179;color:rgba(52,58,64,.8)}.dark-mode .navbar-teal.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-teal.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-teal.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-teal.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-teal.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-teal.navbar-light .form-control-navbar:focus,.dark-mode .navbar-teal.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#1cb386;border-color:#1aa179!important;color:#343a40}.dark-mode .navbar-teal.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-teal.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-teal.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-teal.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-teal.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-teal.navbar-dark .form-control-navbar,.dark-mode .navbar-teal.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#23dba4;border-color:#38dfae;color:rgba(255,255,255,.8)}.dark-mode .navbar-teal.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-teal.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-teal.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-teal.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-teal.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-teal.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-teal.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#26dca6;border-color:#38dfae!important;color:#fff}.dark-mode .navbar-cyan{background-color:#3498db;color:#fff}.dark-mode .navbar-cyan.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-cyan.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-cyan.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-cyan.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-cyan.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-cyan.navbar-light .form-control-navbar,.dark-mode .navbar-cyan.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#268fd5;border-color:#2280bf;color:rgba(52,58,64,.8)}.dark-mode .navbar-cyan.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-cyan.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-cyan.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-cyan.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-cyan.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-cyan.navbar-light .form-control-navbar:focus,.dark-mode .navbar-cyan.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#258cd1;border-color:#2280bf!important;color:#343a40}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar,.dark-mode .navbar-cyan.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#45a1de;border-color:#5bace2;color:rgba(255,255,255,.8)}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-cyan.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#4aa3df;border-color:#5bace2!important;color:#fff}.dark-mode .navbar-white{background-color:#fff;color:#1f2d3d}.dark-mode .navbar-white.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-white.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-white.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-white.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-white.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-white.navbar-light .form-control-navbar,.dark-mode .navbar-white.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#f5f5f5;border-color:#e8e8e8;color:rgba(52,58,64,.8)}.dark-mode .navbar-white.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-white.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-white.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-white.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-white.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-white.navbar-light .form-control-navbar:focus,.dark-mode .navbar-white.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#f2f2f2;border-color:#e8e8e8!important;color:#343a40}.dark-mode .navbar-white.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-white.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-white.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-white.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-white.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-white.navbar-dark .form-control-navbar,.dark-mode .navbar-white.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#fff;border-color:#fff;color:rgba(255,255,255,.8)}.dark-mode .navbar-white.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-white.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-white.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-white.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-white.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-white.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-white.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#fff;border-color:#fff!important;color:#fff}.dark-mode .navbar-gray{background-color:#6c757d;color:#fff}.dark-mode .navbar-gray.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray.navbar-light .form-control-navbar,.dark-mode .navbar-gray.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#636b72;border-color:#575e64;color:rgba(52,58,64,.8)}.dark-mode .navbar-gray.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-gray.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-gray.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-gray.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-gray.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-gray.navbar-light .form-control-navbar:focus,.dark-mode .navbar-gray.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#60686f;border-color:#575e64!important;color:#343a40}.dark-mode .navbar-gray.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray.navbar-dark .form-control-navbar,.dark-mode .navbar-gray.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#757f88;border-color:#838c94;color:rgba(255,255,255,.8)}.dark-mode .navbar-gray.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-gray.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-gray.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-gray.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-gray.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-gray.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-gray.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#78828a;border-color:#838c94!important;color:#fff}.dark-mode .navbar-gray-dark{background-color:#343a40;color:#fff}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar::placeholder{color:rgba(52,58,64,.8)}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar,.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{background-color:#2b3035;border-color:#1f2327;color:rgba(52,58,64,.8)}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:focus::-webkit-input-placeholder{color:#343a40}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:focus::-moz-placeholder{color:#343a40}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:focus:-ms-input-placeholder{color:#343a40}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:focus::-ms-input-placeholder{color:#343a40}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:focus::placeholder{color:#343a40}.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:focus,.dark-mode .navbar-gray-dark.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#292d32;border-color:#1f2327!important;color:#343a40}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.8)}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar,.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{background-color:#3d444b;border-color:#495159;color:rgba(255,255,255,.8)}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:focus::-webkit-input-placeholder{color:#fff}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:focus::-moz-placeholder{color:#fff}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:focus:-ms-input-placeholder{color:#fff}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:focus::-ms-input-placeholder{color:#fff}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:focus::placeholder{color:#fff}.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:focus,.dark-mode .navbar-gray-dark.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#3f474e;border-color:#495159!important;color:#fff}.pagination-month .page-item{justify-self:stretch}.pagination-month .page-item .page-link{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;box-shadow:none}.pagination-month .page-item:first-child .page-link,.pagination-month .page-item:last-child .page-link{height:100%;font-size:1.25rem}.pagination-month .page-item .page-month{margin-bottom:0;font-size:1.25rem;font-weight:700}.pagination-month .page-item .page-year{margin-bottom:0}.pagination-month.pagination-lg .page-month{font-size:1.5625rem}.pagination-month.pagination-sm .page-month{font-size:1rem}.dark-mode .page-item.disabled .page-link,.dark-mode .page-item.disabled a{background-color:#3a4047!important;border-color:#6c757d!important;color:#6c757d}.dark-mode .page-item .page-link{color:#3f6791}.dark-mode .page-item.active .page-link{background-color:#3f6791;color:#fff}.dark-mode .page-item.active .page-link:focus,.dark-mode .page-item.active .page-link:hover{color:#ced4da!important}.dark-mode .page-item:not(.active) .page-link{background-color:#343a40;border-color:#6c757d}.dark-mode .page-item:not(.active) .page-link:focus,.dark-mode .page-item:not(.active) .page-link:hover{color:#4774a3;background-color:#3f474e}.form-group.has-icon{position:relative}.form-group.has-icon .form-control{padding-right:35px}.form-group.has-icon .form-icon{background-color:transparent;border:0;cursor:pointer;font-size:1rem;padding:.375rem .75rem;position:absolute;right:3px;top:0}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.form-control-feedback.fa,.form-control-feedback.fab,.form-control-feedback.fad,.form-control-feedback.fal,.form-control-feedback.far,.form-control-feedback.fas,.form-control-feedback.ion,.form-control-feedback.svg-inline--fa{line-height:calc(2.25rem + 2px)}.input-group-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fab,.input-group-lg+.form-control-feedback.fad,.input-group-lg+.form-control-feedback.fal,.input-group-lg+.form-control-feedback.far,.input-group-lg+.form-control-feedback.fas,.input-group-lg+.form-control-feedback.ion,.input-group-lg+.form-control-feedback.svg-inline--fa,.input-lg+.form-control-feedback.fa,.input-lg+.form-control-feedback.fab,.input-lg+.form-control-feedback.fad,.input-lg+.form-control-feedback.fal,.input-lg+.form-control-feedback.far,.input-lg+.form-control-feedback.fas,.input-lg+.form-control-feedback.ion,.input-lg+.form-control-feedback.svg-inline--fa{line-height:calc(2.875rem + 2px)}.form-group-lg .form-control+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fab,.form-group-lg .form-control+.form-control-feedback.fad,.form-group-lg .form-control+.form-control-feedback.fal,.form-group-lg .form-control+.form-control-feedback.far,.form-group-lg .form-control+.form-control-feedback.fas,.form-group-lg .form-control+.form-control-feedback.ion,.form-group-lg .form-control+.form-control-feedback.svg-inline--fa{line-height:calc(2.875rem + 2px)}.input-group-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fab,.input-group-sm+.form-control-feedback.fad,.input-group-sm+.form-control-feedback.fal,.input-group-sm+.form-control-feedback.far,.input-group-sm+.form-control-feedback.fas,.input-group-sm+.form-control-feedback.ion,.input-group-sm+.form-control-feedback.svg-inline--fa,.input-sm+.form-control-feedback.fa,.input-sm+.form-control-feedback.fab,.input-sm+.form-control-feedback.fad,.input-sm+.form-control-feedback.fal,.input-sm+.form-control-feedback.far,.input-sm+.form-control-feedback.fas,.input-sm+.form-control-feedback.ion,.input-sm+.form-control-feedback.svg-inline--fa{line-height:calc(1.8125rem + 2px)}.form-group-sm .form-control+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fab,.form-group-sm .form-control+.form-control-feedback.fad,.form-group-sm .form-control+.form-control-feedback.fal,.form-group-sm .form-control+.form-control-feedback.far,.form-group-sm .form-control+.form-control-feedback.fas,.form-group-sm .form-control+.form-control-feedback.ion,.form-group-sm .form-control+.form-control-feedback.svg-inline--fa{line-height:calc(1.8125rem + 2px)}label:not(.form-check-label):not(.custom-file-label){font-weight:700}.warning-feedback{font-size:80%;color:#ffc107;display:none;margin-top:.25rem;width:100%}.warning-tooltip{border-radius:.25rem;font-size:.875rem;background-color:rgba(255,193,7,.9);color:#1f2d3d;display:none;line-height:1.5;margin-top:.1rem;max-width:100%;padding:.25rem .5rem;position:absolute;top:100%;z-index:5}.form-control.is-warning{border-color:#ffc107}.form-control.is-warning:focus{border-color:#ffc107;box-shadow:0 0 0 0 rgba(255,193,7,.25)}.form-control.is-warning~.warning-feedback,.form-control.is-warning~.warning-tooltip{display:block}textarea.form-control.is-warning{padding-right:2.25rem;background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-warning{border-color:#ffc107}.custom-select.is-warning:focus{border-color:#ffc107;box-shadow:0 0 0 0 rgba(255,193,7,.25)}.custom-select.is-warning~.warning-feedback,.custom-select.is-warning~.warning-tooltip{display:block}.form-control-file.is-warning~.warning-feedback,.form-control-file.is-warning~.warning-tooltip{display:block}.form-check-input.is-warning~.form-check-label{color:#ffc107}.form-check-input.is-warning~.warning-feedback,.form-check-input.is-warning~.warning-tooltip{display:block}.custom-control-input.is-warning~.custom-control-label{color:#ffc107}.custom-control-input.is-warning~.custom-control-label::before{border-color:#ffc107}.custom-control-input.is-warning~.warning-feedback,.custom-control-input.is-warning~.warning-tooltip{display:block}.custom-control-input.is-warning:checked~.custom-control-label::before{background-color:#ffce3a;border-color:#ffce3a}.custom-control-input.is-warning:focus~.custom-control-label::before{box-shadow:0 0 0 0 rgba(255,193,7,.25)}.custom-control-input.is-warning:focus:not(:checked)~.custom-control-label::before{border-color:#ffc107}.custom-file-input.is-warning~.custom-file-label{border-color:#ffc107}.custom-file-input.is-warning~.warning-feedback,.custom-file-input.is-warning~.warning-tooltip{display:block}.custom-file-input.is-warning:focus~.custom-file-label{border-color:#ffc107;box-shadow:0 0 0 0 rgba(255,193,7,.25)}body.text-sm .input-group-text{font-size:.875rem}.custom-select.form-control-border,.form-control.form-control-border{border-top:0;border-left:0;border-right:0;border-radius:0;box-shadow:inherit}.custom-select.form-control-border.border-width-2,.form-control.form-control-border.border-width-2{border-bottom-width:2px}.custom-select.form-control-border.border-width-3,.form-control.form-control-border.border-width-3{border-bottom-width:3px}.custom-switch.custom-switch-off-primary .custom-control-input~.custom-control-label::before{background-color:#007bff;border-color:#004a99}.custom-switch.custom-switch-off-primary .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-off-primary .custom-control-input~.custom-control-label::after{background-color:#003e80}.custom-switch.custom-switch-on-primary .custom-control-input:checked~.custom-control-label::before{background-color:#007bff;border-color:#004a99}.custom-switch.custom-switch-on-primary .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-on-primary .custom-control-input:checked~.custom-control-label::after{background-color:#99caff}.custom-switch.custom-switch-off-secondary .custom-control-input~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-off-secondary .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-off-secondary .custom-control-input~.custom-control-label::after{background-color:#313539}.custom-switch.custom-switch-on-secondary .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-on-secondary .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-on-secondary .custom-control-input:checked~.custom-control-label::after{background-color:#bcc1c6}.custom-switch.custom-switch-off-success .custom-control-input~.custom-control-label::before{background-color:#28a745;border-color:#145523}.custom-switch.custom-switch-off-success .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-off-success .custom-control-input~.custom-control-label::after{background-color:#0f401b}.custom-switch.custom-switch-on-success .custom-control-input:checked~.custom-control-label::before{background-color:#28a745;border-color:#145523}.custom-switch.custom-switch-on-success .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-on-success .custom-control-input:checked~.custom-control-label::after{background-color:#86e29b}.custom-switch.custom-switch-off-info .custom-control-input~.custom-control-label::before{background-color:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-off-info .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-off-info .custom-control-input~.custom-control-label::after{background-color:#093e47}.custom-switch.custom-switch-on-info .custom-control-input:checked~.custom-control-label::before{background-color:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-on-info .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-on-info .custom-control-input:checked~.custom-control-label::after{background-color:#7adeee}.custom-switch.custom-switch-off-warning .custom-control-input~.custom-control-label::before{background-color:#ffc107;border-color:#a07800}.custom-switch.custom-switch-off-warning .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-off-warning .custom-control-input~.custom-control-label::after{background-color:#876500}.custom-switch.custom-switch-on-warning .custom-control-input:checked~.custom-control-label::before{background-color:#ffc107;border-color:#a07800}.custom-switch.custom-switch-on-warning .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-on-warning .custom-control-input:checked~.custom-control-label::after{background-color:#ffe7a0}.custom-switch.custom-switch-off-danger .custom-control-input~.custom-control-label::before{background-color:#dc3545;border-color:#921925}.custom-switch.custom-switch-off-danger .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-off-danger .custom-control-input~.custom-control-label::after{background-color:#7c151f}.custom-switch.custom-switch-on-danger .custom-control-input:checked~.custom-control-label::before{background-color:#dc3545;border-color:#921925}.custom-switch.custom-switch-on-danger .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-on-danger .custom-control-input:checked~.custom-control-label::after{background-color:#f3b7bd}.custom-switch.custom-switch-off-light .custom-control-input~.custom-control-label::before{background-color:#f8f9fa;border-color:#bdc6d0}.custom-switch.custom-switch-off-light .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-switch.custom-switch-off-light .custom-control-input~.custom-control-label::after{background-color:#aeb9c5}.custom-switch.custom-switch-on-light .custom-control-input:checked~.custom-control-label::before{background-color:#f8f9fa;border-color:#bdc6d0}.custom-switch.custom-switch-on-light .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-switch.custom-switch-on-light .custom-control-input:checked~.custom-control-label::after{background-color:#fff}.custom-switch.custom-switch-off-dark .custom-control-input~.custom-control-label::before{background-color:#343a40;border-color:#060708}.custom-switch.custom-switch-off-dark .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-off-dark .custom-control-input~.custom-control-label::after{background-color:#000}.custom-switch.custom-switch-on-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.custom-switch.custom-switch-on-dark .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-on-dark .custom-control-input:checked~.custom-control-label::after{background-color:#7a8793}.custom-switch.custom-switch-off-lightblue .custom-control-input~.custom-control-label::before{background-color:#3c8dbc;border-color:#23536f}.custom-switch.custom-switch-off-lightblue .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-switch.custom-switch-off-lightblue .custom-control-input~.custom-control-label::after{background-color:#1d455b}.custom-switch.custom-switch-on-lightblue .custom-control-input:checked~.custom-control-label::before{background-color:#3c8dbc;border-color:#23536f}.custom-switch.custom-switch-on-lightblue .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-switch.custom-switch-on-lightblue .custom-control-input:checked~.custom-control-label::after{background-color:#acd0e5}.custom-switch.custom-switch-off-navy .custom-control-input~.custom-control-label::before{background-color:#001f3f;border-color:#000}.custom-switch.custom-switch-off-navy .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-switch.custom-switch-off-navy .custom-control-input~.custom-control-label::after{background-color:#000}.custom-switch.custom-switch-on-navy .custom-control-input:checked~.custom-control-label::before{background-color:#001f3f;border-color:#000}.custom-switch.custom-switch-on-navy .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-switch.custom-switch-on-navy .custom-control-input:checked~.custom-control-label::after{background-color:#006ad8}.custom-switch.custom-switch-off-olive .custom-control-input~.custom-control-label::before{background-color:#3d9970;border-color:#20503b}.custom-switch.custom-switch-off-olive .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-switch.custom-switch-off-olive .custom-control-input~.custom-control-label::after{background-color:#193e2d}.custom-switch.custom-switch-on-olive .custom-control-input:checked~.custom-control-label::before{background-color:#3d9970;border-color:#20503b}.custom-switch.custom-switch-on-olive .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-switch.custom-switch-on-olive .custom-control-input:checked~.custom-control-label::after{background-color:#99d6bb}.custom-switch.custom-switch-off-lime .custom-control-input~.custom-control-label::before{background-color:#01ff70;border-color:#009a43}.custom-switch.custom-switch-off-lime .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-switch.custom-switch-off-lime .custom-control-input~.custom-control-label::after{background-color:#008138}.custom-switch.custom-switch-on-lime .custom-control-input:checked~.custom-control-label::before{background-color:#01ff70;border-color:#009a43}.custom-switch.custom-switch-on-lime .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-switch.custom-switch-on-lime .custom-control-input:checked~.custom-control-label::after{background-color:#9affc6}.custom-switch.custom-switch-off-fuchsia .custom-control-input~.custom-control-label::before{background-color:#f012be;border-color:#930974}.custom-switch.custom-switch-off-fuchsia .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-switch.custom-switch-off-fuchsia .custom-control-input~.custom-control-label::after{background-color:#7b0861}.custom-switch.custom-switch-on-fuchsia .custom-control-input:checked~.custom-control-label::before{background-color:#f012be;border-color:#930974}.custom-switch.custom-switch-on-fuchsia .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-switch.custom-switch-on-fuchsia .custom-control-input:checked~.custom-control-label::after{background-color:#f9a2e5}.custom-switch.custom-switch-off-maroon .custom-control-input~.custom-control-label::before{background-color:#d81b60;border-color:#7d1038}.custom-switch.custom-switch-off-maroon .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-switch.custom-switch-off-maroon .custom-control-input~.custom-control-label::after{background-color:#670d2e}.custom-switch.custom-switch-on-maroon .custom-control-input:checked~.custom-control-label::before{background-color:#d81b60;border-color:#7d1038}.custom-switch.custom-switch-on-maroon .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-switch.custom-switch-on-maroon .custom-control-input:checked~.custom-control-label::after{background-color:#f29aba}.custom-switch.custom-switch-off-blue .custom-control-input~.custom-control-label::before{background-color:#007bff;border-color:#004a99}.custom-switch.custom-switch-off-blue .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-off-blue .custom-control-input~.custom-control-label::after{background-color:#003e80}.custom-switch.custom-switch-on-blue .custom-control-input:checked~.custom-control-label::before{background-color:#007bff;border-color:#004a99}.custom-switch.custom-switch-on-blue .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-on-blue .custom-control-input:checked~.custom-control-label::after{background-color:#99caff}.custom-switch.custom-switch-off-indigo .custom-control-input~.custom-control-label::before{background-color:#6610f2;border-color:#3d0894}.custom-switch.custom-switch-off-indigo .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-switch.custom-switch-off-indigo .custom-control-input~.custom-control-label::after{background-color:#33077c}.custom-switch.custom-switch-on-indigo .custom-control-input:checked~.custom-control-label::before{background-color:#6610f2;border-color:#3d0894}.custom-switch.custom-switch-on-indigo .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-switch.custom-switch-on-indigo .custom-control-input:checked~.custom-control-label::after{background-color:#c3a1fa}.custom-switch.custom-switch-off-purple .custom-control-input~.custom-control-label::before{background-color:#6f42c1;border-color:#432776}.custom-switch.custom-switch-off-purple .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-switch.custom-switch-off-purple .custom-control-input~.custom-control-label::after{background-color:#382063}.custom-switch.custom-switch-on-purple .custom-control-input:checked~.custom-control-label::before{background-color:#6f42c1;border-color:#432776}.custom-switch.custom-switch-on-purple .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-switch.custom-switch-on-purple .custom-control-input:checked~.custom-control-label::after{background-color:#c7b5e7}.custom-switch.custom-switch-off-pink .custom-control-input~.custom-control-label::before{background-color:#e83e8c;border-color:#ac145a}.custom-switch.custom-switch-off-pink .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-switch.custom-switch-off-pink .custom-control-input~.custom-control-label::after{background-color:#95124e}.custom-switch.custom-switch-on-pink .custom-control-input:checked~.custom-control-label::before{background-color:#e83e8c;border-color:#ac145a}.custom-switch.custom-switch-on-pink .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-switch.custom-switch-on-pink .custom-control-input:checked~.custom-control-label::after{background-color:#f8c7dd}.custom-switch.custom-switch-off-red .custom-control-input~.custom-control-label::before{background-color:#dc3545;border-color:#921925}.custom-switch.custom-switch-off-red .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-off-red .custom-control-input~.custom-control-label::after{background-color:#7c151f}.custom-switch.custom-switch-on-red .custom-control-input:checked~.custom-control-label::before{background-color:#dc3545;border-color:#921925}.custom-switch.custom-switch-on-red .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-on-red .custom-control-input:checked~.custom-control-label::after{background-color:#f3b7bd}.custom-switch.custom-switch-off-orange .custom-control-input~.custom-control-label::before{background-color:#fd7e14;border-color:#aa4e01}.custom-switch.custom-switch-off-orange .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-switch.custom-switch-off-orange .custom-control-input~.custom-control-label::after{background-color:#904201}.custom-switch.custom-switch-on-orange .custom-control-input:checked~.custom-control-label::before{background-color:#fd7e14;border-color:#aa4e01}.custom-switch.custom-switch-on-orange .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-switch.custom-switch-on-orange .custom-control-input:checked~.custom-control-label::after{background-color:#fed1ac}.custom-switch.custom-switch-off-yellow .custom-control-input~.custom-control-label::before{background-color:#ffc107;border-color:#a07800}.custom-switch.custom-switch-off-yellow .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-off-yellow .custom-control-input~.custom-control-label::after{background-color:#876500}.custom-switch.custom-switch-on-yellow .custom-control-input:checked~.custom-control-label::before{background-color:#ffc107;border-color:#a07800}.custom-switch.custom-switch-on-yellow .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-on-yellow .custom-control-input:checked~.custom-control-label::after{background-color:#ffe7a0}.custom-switch.custom-switch-off-green .custom-control-input~.custom-control-label::before{background-color:#28a745;border-color:#145523}.custom-switch.custom-switch-off-green .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-off-green .custom-control-input~.custom-control-label::after{background-color:#0f401b}.custom-switch.custom-switch-on-green .custom-control-input:checked~.custom-control-label::before{background-color:#28a745;border-color:#145523}.custom-switch.custom-switch-on-green .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-on-green .custom-control-input:checked~.custom-control-label::after{background-color:#86e29b}.custom-switch.custom-switch-off-teal .custom-control-input~.custom-control-label::before{background-color:#20c997;border-color:#127155}.custom-switch.custom-switch-off-teal .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-switch.custom-switch-off-teal .custom-control-input~.custom-control-label::after{background-color:#0e5b44}.custom-switch.custom-switch-on-teal .custom-control-input:checked~.custom-control-label::before{background-color:#20c997;border-color:#127155}.custom-switch.custom-switch-on-teal .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-switch.custom-switch-on-teal .custom-control-input:checked~.custom-control-label::after{background-color:#94eed3}.custom-switch.custom-switch-off-cyan .custom-control-input~.custom-control-label::before{background-color:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-off-cyan .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-off-cyan .custom-control-input~.custom-control-label::after{background-color:#093e47}.custom-switch.custom-switch-on-cyan .custom-control-input:checked~.custom-control-label::before{background-color:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-on-cyan .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-on-cyan .custom-control-input:checked~.custom-control-label::after{background-color:#7adeee}.custom-switch.custom-switch-off-white .custom-control-input~.custom-control-label::before{background-color:#fff;border-color:#ccc}.custom-switch.custom-switch-off-white .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-switch.custom-switch-off-white .custom-control-input~.custom-control-label::after{background-color:#bfbfbf}.custom-switch.custom-switch-on-white .custom-control-input:checked~.custom-control-label::before{background-color:#fff;border-color:#ccc}.custom-switch.custom-switch-on-white .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-switch.custom-switch-on-white .custom-control-input:checked~.custom-control-label::after{background-color:#fff}.custom-switch.custom-switch-off-gray .custom-control-input~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-off-gray .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-off-gray .custom-control-input~.custom-control-label::after{background-color:#313539}.custom-switch.custom-switch-on-gray .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-on-gray .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-on-gray .custom-control-input:checked~.custom-control-label::after{background-color:#bcc1c6}.custom-switch.custom-switch-off-gray-dark .custom-control-input~.custom-control-label::before{background-color:#343a40;border-color:#060708}.custom-switch.custom-switch-off-gray-dark .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-off-gray-dark .custom-control-input~.custom-control-label::after{background-color:#000}.custom-switch.custom-switch-on-gray-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.custom-switch.custom-switch-on-gray-dark .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-on-gray-dark .custom-control-input:checked~.custom-control-label::after{background-color:#7a8793}.custom-range.custom-range-primary:focus{outline:0}.custom-range.custom-range-primary:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-primary:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-primary:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-primary::-webkit-slider-thumb{background-color:#007bff}.custom-range.custom-range-primary::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-primary::-moz-range-thumb{background-color:#007bff}.custom-range.custom-range-primary::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-primary::-ms-thumb{background-color:#007bff}.custom-range.custom-range-primary::-ms-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-secondary:focus{outline:0}.custom-range.custom-range-secondary:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-secondary:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-secondary:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-secondary::-webkit-slider-thumb{background-color:#6c757d}.custom-range.custom-range-secondary::-webkit-slider-thumb:active{background-color:#caced1}.custom-range.custom-range-secondary::-moz-range-thumb{background-color:#6c757d}.custom-range.custom-range-secondary::-moz-range-thumb:active{background-color:#caced1}.custom-range.custom-range-secondary::-ms-thumb{background-color:#6c757d}.custom-range.custom-range-secondary::-ms-thumb:active{background-color:#caced1}.custom-range.custom-range-success:focus{outline:0}.custom-range.custom-range-success:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-success:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-success:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-success::-webkit-slider-thumb{background-color:#28a745}.custom-range.custom-range-success::-webkit-slider-thumb:active{background-color:#9be7ac}.custom-range.custom-range-success::-moz-range-thumb{background-color:#28a745}.custom-range.custom-range-success::-moz-range-thumb:active{background-color:#9be7ac}.custom-range.custom-range-success::-ms-thumb{background-color:#28a745}.custom-range.custom-range-success::-ms-thumb:active{background-color:#9be7ac}.custom-range.custom-range-info:focus{outline:0}.custom-range.custom-range-info:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-info:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-info:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-info::-webkit-slider-thumb{background-color:#17a2b8}.custom-range.custom-range-info::-webkit-slider-thumb:active{background-color:#90e4f1}.custom-range.custom-range-info::-moz-range-thumb{background-color:#17a2b8}.custom-range.custom-range-info::-moz-range-thumb:active{background-color:#90e4f1}.custom-range.custom-range-info::-ms-thumb{background-color:#17a2b8}.custom-range.custom-range-info::-ms-thumb:active{background-color:#90e4f1}.custom-range.custom-range-warning:focus{outline:0}.custom-range.custom-range-warning:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-warning:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-warning:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-warning::-webkit-slider-thumb{background-color:#ffc107}.custom-range.custom-range-warning::-webkit-slider-thumb:active{background-color:#ffeeba}.custom-range.custom-range-warning::-moz-range-thumb{background-color:#ffc107}.custom-range.custom-range-warning::-moz-range-thumb:active{background-color:#ffeeba}.custom-range.custom-range-warning::-ms-thumb{background-color:#ffc107}.custom-range.custom-range-warning::-ms-thumb:active{background-color:#ffeeba}.custom-range.custom-range-danger:focus{outline:0}.custom-range.custom-range-danger:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-danger:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-danger:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-danger::-webkit-slider-thumb{background-color:#dc3545}.custom-range.custom-range-danger::-webkit-slider-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-danger::-moz-range-thumb{background-color:#dc3545}.custom-range.custom-range-danger::-moz-range-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-danger::-ms-thumb{background-color:#dc3545}.custom-range.custom-range-danger::-ms-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-light:focus{outline:0}.custom-range.custom-range-light:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-range.custom-range-light:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-range.custom-range-light:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-range.custom-range-light::-webkit-slider-thumb{background-color:#f8f9fa}.custom-range.custom-range-light::-webkit-slider-thumb:active{background-color:#fff}.custom-range.custom-range-light::-moz-range-thumb{background-color:#f8f9fa}.custom-range.custom-range-light::-moz-range-thumb:active{background-color:#fff}.custom-range.custom-range-light::-ms-thumb{background-color:#f8f9fa}.custom-range.custom-range-light::-ms-thumb:active{background-color:#fff}.custom-range.custom-range-dark:focus{outline:0}.custom-range.custom-range-dark:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-dark:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-dark:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-dark::-webkit-slider-thumb{background-color:#343a40}.custom-range.custom-range-dark::-webkit-slider-thumb:active{background-color:#88939e}.custom-range.custom-range-dark::-moz-range-thumb{background-color:#343a40}.custom-range.custom-range-dark::-moz-range-thumb:active{background-color:#88939e}.custom-range.custom-range-dark::-ms-thumb{background-color:#343a40}.custom-range.custom-range-dark::-ms-thumb:active{background-color:#88939e}.custom-range.custom-range-lightblue:focus{outline:0}.custom-range.custom-range-lightblue:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-range.custom-range-lightblue:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-range.custom-range-lightblue:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-range.custom-range-lightblue::-webkit-slider-thumb{background-color:#3c8dbc}.custom-range.custom-range-lightblue::-webkit-slider-thumb:active{background-color:#c0dbeb}.custom-range.custom-range-lightblue::-moz-range-thumb{background-color:#3c8dbc}.custom-range.custom-range-lightblue::-moz-range-thumb:active{background-color:#c0dbeb}.custom-range.custom-range-lightblue::-ms-thumb{background-color:#3c8dbc}.custom-range.custom-range-lightblue::-ms-thumb:active{background-color:#c0dbeb}.custom-range.custom-range-navy:focus{outline:0}.custom-range.custom-range-navy:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-range.custom-range-navy:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-range.custom-range-navy:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-range.custom-range-navy::-webkit-slider-thumb{background-color:#001f3f}.custom-range.custom-range-navy::-webkit-slider-thumb:active{background-color:#0077f2}.custom-range.custom-range-navy::-moz-range-thumb{background-color:#001f3f}.custom-range.custom-range-navy::-moz-range-thumb:active{background-color:#0077f2}.custom-range.custom-range-navy::-ms-thumb{background-color:#001f3f}.custom-range.custom-range-navy::-ms-thumb:active{background-color:#0077f2}.custom-range.custom-range-olive:focus{outline:0}.custom-range.custom-range-olive:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-range.custom-range-olive:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-range.custom-range-olive:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-range.custom-range-olive::-webkit-slider-thumb{background-color:#3d9970}.custom-range.custom-range-olive::-webkit-slider-thumb:active{background-color:#abdec7}.custom-range.custom-range-olive::-moz-range-thumb{background-color:#3d9970}.custom-range.custom-range-olive::-moz-range-thumb:active{background-color:#abdec7}.custom-range.custom-range-olive::-ms-thumb{background-color:#3d9970}.custom-range.custom-range-olive::-ms-thumb:active{background-color:#abdec7}.custom-range.custom-range-lime:focus{outline:0}.custom-range.custom-range-lime:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-range.custom-range-lime:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-range.custom-range-lime:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-range.custom-range-lime::-webkit-slider-thumb{background-color:#01ff70}.custom-range.custom-range-lime::-webkit-slider-thumb:active{background-color:#b4ffd4}.custom-range.custom-range-lime::-moz-range-thumb{background-color:#01ff70}.custom-range.custom-range-lime::-moz-range-thumb:active{background-color:#b4ffd4}.custom-range.custom-range-lime::-ms-thumb{background-color:#01ff70}.custom-range.custom-range-lime::-ms-thumb:active{background-color:#b4ffd4}.custom-range.custom-range-fuchsia:focus{outline:0}.custom-range.custom-range-fuchsia:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-range.custom-range-fuchsia:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-range.custom-range-fuchsia:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-range.custom-range-fuchsia::-webkit-slider-thumb{background-color:#f012be}.custom-range.custom-range-fuchsia::-webkit-slider-thumb:active{background-color:#fbbaec}.custom-range.custom-range-fuchsia::-moz-range-thumb{background-color:#f012be}.custom-range.custom-range-fuchsia::-moz-range-thumb:active{background-color:#fbbaec}.custom-range.custom-range-fuchsia::-ms-thumb{background-color:#f012be}.custom-range.custom-range-fuchsia::-ms-thumb:active{background-color:#fbbaec}.custom-range.custom-range-maroon:focus{outline:0}.custom-range.custom-range-maroon:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-range.custom-range-maroon:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-range.custom-range-maroon:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-range.custom-range-maroon::-webkit-slider-thumb{background-color:#d81b60}.custom-range.custom-range-maroon::-webkit-slider-thumb:active{background-color:#f5b0c9}.custom-range.custom-range-maroon::-moz-range-thumb{background-color:#d81b60}.custom-range.custom-range-maroon::-moz-range-thumb:active{background-color:#f5b0c9}.custom-range.custom-range-maroon::-ms-thumb{background-color:#d81b60}.custom-range.custom-range-maroon::-ms-thumb:active{background-color:#f5b0c9}.custom-range.custom-range-blue:focus{outline:0}.custom-range.custom-range-blue:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-blue:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-blue:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-blue::-webkit-slider-thumb{background-color:#007bff}.custom-range.custom-range-blue::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-blue::-moz-range-thumb{background-color:#007bff}.custom-range.custom-range-blue::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-blue::-ms-thumb{background-color:#007bff}.custom-range.custom-range-blue::-ms-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-indigo:focus{outline:0}.custom-range.custom-range-indigo:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-range.custom-range-indigo:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-range.custom-range-indigo:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-range.custom-range-indigo::-webkit-slider-thumb{background-color:#6610f2}.custom-range.custom-range-indigo::-webkit-slider-thumb:active{background-color:#d2b9fb}.custom-range.custom-range-indigo::-moz-range-thumb{background-color:#6610f2}.custom-range.custom-range-indigo::-moz-range-thumb:active{background-color:#d2b9fb}.custom-range.custom-range-indigo::-ms-thumb{background-color:#6610f2}.custom-range.custom-range-indigo::-ms-thumb:active{background-color:#d2b9fb}.custom-range.custom-range-purple:focus{outline:0}.custom-range.custom-range-purple:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-range.custom-range-purple:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-range.custom-range-purple:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-range.custom-range-purple::-webkit-slider-thumb{background-color:#6f42c1}.custom-range.custom-range-purple::-webkit-slider-thumb:active{background-color:#d5c8ed}.custom-range.custom-range-purple::-moz-range-thumb{background-color:#6f42c1}.custom-range.custom-range-purple::-moz-range-thumb:active{background-color:#d5c8ed}.custom-range.custom-range-purple::-ms-thumb{background-color:#6f42c1}.custom-range.custom-range-purple::-ms-thumb:active{background-color:#d5c8ed}.custom-range.custom-range-pink:focus{outline:0}.custom-range.custom-range-pink:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-range.custom-range-pink:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-range.custom-range-pink:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-range.custom-range-pink::-webkit-slider-thumb{background-color:#e83e8c}.custom-range.custom-range-pink::-webkit-slider-thumb:active{background-color:#fbddeb}.custom-range.custom-range-pink::-moz-range-thumb{background-color:#e83e8c}.custom-range.custom-range-pink::-moz-range-thumb:active{background-color:#fbddeb}.custom-range.custom-range-pink::-ms-thumb{background-color:#e83e8c}.custom-range.custom-range-pink::-ms-thumb:active{background-color:#fbddeb}.custom-range.custom-range-red:focus{outline:0}.custom-range.custom-range-red:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-red:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-red:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-red::-webkit-slider-thumb{background-color:#dc3545}.custom-range.custom-range-red::-webkit-slider-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-red::-moz-range-thumb{background-color:#dc3545}.custom-range.custom-range-red::-moz-range-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-red::-ms-thumb{background-color:#dc3545}.custom-range.custom-range-red::-ms-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-orange:focus{outline:0}.custom-range.custom-range-orange:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-range.custom-range-orange:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-range.custom-range-orange:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-range.custom-range-orange::-webkit-slider-thumb{background-color:#fd7e14}.custom-range.custom-range-orange::-webkit-slider-thumb:active{background-color:#ffdfc5}.custom-range.custom-range-orange::-moz-range-thumb{background-color:#fd7e14}.custom-range.custom-range-orange::-moz-range-thumb:active{background-color:#ffdfc5}.custom-range.custom-range-orange::-ms-thumb{background-color:#fd7e14}.custom-range.custom-range-orange::-ms-thumb:active{background-color:#ffdfc5}.custom-range.custom-range-yellow:focus{outline:0}.custom-range.custom-range-yellow:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-yellow:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-yellow:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-yellow::-webkit-slider-thumb{background-color:#ffc107}.custom-range.custom-range-yellow::-webkit-slider-thumb:active{background-color:#ffeeba}.custom-range.custom-range-yellow::-moz-range-thumb{background-color:#ffc107}.custom-range.custom-range-yellow::-moz-range-thumb:active{background-color:#ffeeba}.custom-range.custom-range-yellow::-ms-thumb{background-color:#ffc107}.custom-range.custom-range-yellow::-ms-thumb:active{background-color:#ffeeba}.custom-range.custom-range-green:focus{outline:0}.custom-range.custom-range-green:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-green:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-green:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-green::-webkit-slider-thumb{background-color:#28a745}.custom-range.custom-range-green::-webkit-slider-thumb:active{background-color:#9be7ac}.custom-range.custom-range-green::-moz-range-thumb{background-color:#28a745}.custom-range.custom-range-green::-moz-range-thumb:active{background-color:#9be7ac}.custom-range.custom-range-green::-ms-thumb{background-color:#28a745}.custom-range.custom-range-green::-ms-thumb:active{background-color:#9be7ac}.custom-range.custom-range-teal:focus{outline:0}.custom-range.custom-range-teal:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-range.custom-range-teal:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-range.custom-range-teal:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-range.custom-range-teal::-webkit-slider-thumb{background-color:#20c997}.custom-range.custom-range-teal::-webkit-slider-thumb:active{background-color:#aaf1dc}.custom-range.custom-range-teal::-moz-range-thumb{background-color:#20c997}.custom-range.custom-range-teal::-moz-range-thumb:active{background-color:#aaf1dc}.custom-range.custom-range-teal::-ms-thumb{background-color:#20c997}.custom-range.custom-range-teal::-ms-thumb:active{background-color:#aaf1dc}.custom-range.custom-range-cyan:focus{outline:0}.custom-range.custom-range-cyan:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-cyan:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-cyan:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-cyan::-webkit-slider-thumb{background-color:#17a2b8}.custom-range.custom-range-cyan::-webkit-slider-thumb:active{background-color:#90e4f1}.custom-range.custom-range-cyan::-moz-range-thumb{background-color:#17a2b8}.custom-range.custom-range-cyan::-moz-range-thumb:active{background-color:#90e4f1}.custom-range.custom-range-cyan::-ms-thumb{background-color:#17a2b8}.custom-range.custom-range-cyan::-ms-thumb:active{background-color:#90e4f1}.custom-range.custom-range-white:focus{outline:0}.custom-range.custom-range-white:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-range.custom-range-white:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-range.custom-range-white:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-range.custom-range-white::-webkit-slider-thumb{background-color:#fff}.custom-range.custom-range-white::-webkit-slider-thumb:active{background-color:#fff}.custom-range.custom-range-white::-moz-range-thumb{background-color:#fff}.custom-range.custom-range-white::-moz-range-thumb:active{background-color:#fff}.custom-range.custom-range-white::-ms-thumb{background-color:#fff}.custom-range.custom-range-white::-ms-thumb:active{background-color:#fff}.custom-range.custom-range-gray:focus{outline:0}.custom-range.custom-range-gray:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-gray:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-gray:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-gray::-webkit-slider-thumb{background-color:#6c757d}.custom-range.custom-range-gray::-webkit-slider-thumb:active{background-color:#caced1}.custom-range.custom-range-gray::-moz-range-thumb{background-color:#6c757d}.custom-range.custom-range-gray::-moz-range-thumb:active{background-color:#caced1}.custom-range.custom-range-gray::-ms-thumb{background-color:#6c757d}.custom-range.custom-range-gray::-ms-thumb:active{background-color:#caced1}.custom-range.custom-range-gray-dark:focus{outline:0}.custom-range.custom-range-gray-dark:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-gray-dark:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-gray-dark:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-gray-dark::-webkit-slider-thumb{background-color:#343a40}.custom-range.custom-range-gray-dark::-webkit-slider-thumb:active{background-color:#88939e}.custom-range.custom-range-gray-dark::-moz-range-thumb{background-color:#343a40}.custom-range.custom-range-gray-dark::-moz-range-thumb:active{background-color:#88939e}.custom-range.custom-range-gray-dark::-ms-thumb{background-color:#343a40}.custom-range.custom-range-gray-dark::-ms-thumb:active{background-color:#88939e}.custom-control-input-primary:checked~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-control-input-primary.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23007bff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-primary.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23007bff'/%3E%3C/svg%3E")!important}.custom-control-input-primary:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input-primary:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input-primary:not(:disabled):active~.custom-control-label::before{background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input-secondary:checked~.custom-control-label::before{border-color:#6c757d;background-color:#6c757d}.custom-control-input-secondary.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236c757d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-secondary.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236c757d'/%3E%3C/svg%3E")!important}.custom-control-input-secondary:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(108,117,125,.25)}.custom-control-input-secondary:focus:not(:checked)~.custom-control-label::before{border-color:#afb5ba}.custom-control-input-secondary:not(:disabled):active~.custom-control-label::before{background-color:#caced1;border-color:#caced1}.custom-control-input-success:checked~.custom-control-label::before{border-color:#28a745;background-color:#28a745}.custom-control-input-success.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-success.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2328a745'/%3E%3C/svg%3E")!important}.custom-control-input-success:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input-success:focus:not(:checked)~.custom-control-label::before{border-color:#71dd8a}.custom-control-input-success:not(:disabled):active~.custom-control-label::before{background-color:#9be7ac;border-color:#9be7ac}.custom-control-input-info:checked~.custom-control-label::before{border-color:#17a2b8;background-color:#17a2b8}.custom-control-input-info.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2317a2b8' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-info.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2317a2b8'/%3E%3C/svg%3E")!important}.custom-control-input-info:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(23,162,184,.25)}.custom-control-input-info:focus:not(:checked)~.custom-control-label::before{border-color:#63d9ec}.custom-control-input-info:not(:disabled):active~.custom-control-label::before{background-color:#90e4f1;border-color:#90e4f1}.custom-control-input-warning:checked~.custom-control-label::before{border-color:#ffc107;background-color:#ffc107}.custom-control-input-warning.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffc107' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-warning.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23ffc107'/%3E%3C/svg%3E")!important}.custom-control-input-warning:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(255,193,7,.25)}.custom-control-input-warning:focus:not(:checked)~.custom-control-label::before{border-color:#ffe187}.custom-control-input-warning:not(:disabled):active~.custom-control-label::before{background-color:#ffeeba;border-color:#ffeeba}.custom-control-input-danger:checked~.custom-control-label::before{border-color:#dc3545;background-color:#dc3545}.custom-control-input-danger.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23dc3545' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-danger.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23dc3545'/%3E%3C/svg%3E")!important}.custom-control-input-danger:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input-danger:focus:not(:checked)~.custom-control-label::before{border-color:#efa2a9}.custom-control-input-danger:not(:disabled):active~.custom-control-label::before{background-color:#f6cdd1;border-color:#f6cdd1}.custom-control-input-light:checked~.custom-control-label::before{border-color:#f8f9fa;background-color:#f8f9fa}.custom-control-input-light.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f8f9fa' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-light.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23f8f9fa'/%3E%3C/svg%3E")!important}.custom-control-input-light:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(248,249,250,.25)}.custom-control-input-light:focus:not(:checked)~.custom-control-label::before{border-color:#fff}.custom-control-input-light:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.custom-control-input-dark:checked~.custom-control-label::before{border-color:#343a40;background-color:#343a40}.custom-control-input-dark.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23343a40' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-dark.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23343a40'/%3E%3C/svg%3E")!important}.custom-control-input-dark:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(52,58,64,.25)}.custom-control-input-dark:focus:not(:checked)~.custom-control-label::before{border-color:#6d7a86}.custom-control-input-dark:not(:disabled):active~.custom-control-label::before{background-color:#88939e;border-color:#88939e}.custom-control-input-lightblue:checked~.custom-control-label::before{border-color:#3c8dbc;background-color:#3c8dbc}.custom-control-input-lightblue.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%233c8dbc' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-lightblue.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%233c8dbc'/%3E%3C/svg%3E")!important}.custom-control-input-lightblue:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(60,141,188,.25)}.custom-control-input-lightblue:focus:not(:checked)~.custom-control-label::before{border-color:#99c5de}.custom-control-input-lightblue:not(:disabled):active~.custom-control-label::before{background-color:#c0dbeb;border-color:#c0dbeb}.custom-control-input-navy:checked~.custom-control-label::before{border-color:#001f3f;background-color:#001f3f}.custom-control-input-navy.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23001f3f' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-navy.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23001f3f'/%3E%3C/svg%3E")!important}.custom-control-input-navy:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(0,31,63,.25)}.custom-control-input-navy:focus:not(:checked)~.custom-control-label::before{border-color:#005ebf}.custom-control-input-navy:not(:disabled):active~.custom-control-label::before{background-color:#0077f2;border-color:#0077f2}.custom-control-input-olive:checked~.custom-control-label::before{border-color:#3d9970;background-color:#3d9970}.custom-control-input-olive.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%233d9970' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-olive.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%233d9970'/%3E%3C/svg%3E")!important}.custom-control-input-olive:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(61,153,112,.25)}.custom-control-input-olive:focus:not(:checked)~.custom-control-label::before{border-color:#87cfaf}.custom-control-input-olive:not(:disabled):active~.custom-control-label::before{background-color:#abdec7;border-color:#abdec7}.custom-control-input-lime:checked~.custom-control-label::before{border-color:#01ff70;background-color:#01ff70}.custom-control-input-lime.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2301ff70' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-lime.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2301ff70'/%3E%3C/svg%3E")!important}.custom-control-input-lime:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(1,255,112,.25)}.custom-control-input-lime:focus:not(:checked)~.custom-control-label::before{border-color:#81ffb8}.custom-control-input-lime:not(:disabled):active~.custom-control-label::before{background-color:#b4ffd4;border-color:#b4ffd4}.custom-control-input-fuchsia:checked~.custom-control-label::before{border-color:#f012be;background-color:#f012be}.custom-control-input-fuchsia.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f012be' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-fuchsia.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23f012be'/%3E%3C/svg%3E")!important}.custom-control-input-fuchsia:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(240,18,190,.25)}.custom-control-input-fuchsia:focus:not(:checked)~.custom-control-label::before{border-color:#f88adf}.custom-control-input-fuchsia:not(:disabled):active~.custom-control-label::before{background-color:#fbbaec;border-color:#fbbaec}.custom-control-input-maroon:checked~.custom-control-label::before{border-color:#d81b60;background-color:#d81b60}.custom-control-input-maroon.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23d81b60' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-maroon.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23d81b60'/%3E%3C/svg%3E")!important}.custom-control-input-maroon:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(216,27,96,.25)}.custom-control-input-maroon:focus:not(:checked)~.custom-control-label::before{border-color:#f083ab}.custom-control-input-maroon:not(:disabled):active~.custom-control-label::before{background-color:#f5b0c9;border-color:#f5b0c9}.custom-control-input-blue:checked~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-control-input-blue.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23007bff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-blue.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23007bff'/%3E%3C/svg%3E")!important}.custom-control-input-blue:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input-blue:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input-blue:not(:disabled):active~.custom-control-label::before{background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input-indigo:checked~.custom-control-label::before{border-color:#6610f2;background-color:#6610f2}.custom-control-input-indigo.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236610f2' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-indigo.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236610f2'/%3E%3C/svg%3E")!important}.custom-control-input-indigo:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(102,16,242,.25)}.custom-control-input-indigo:focus:not(:checked)~.custom-control-label::before{border-color:#b389f9}.custom-control-input-indigo:not(:disabled):active~.custom-control-label::before{background-color:#d2b9fb;border-color:#d2b9fb}.custom-control-input-purple:checked~.custom-control-label::before{border-color:#6f42c1;background-color:#6f42c1}.custom-control-input-purple.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236f42c1' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-purple.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236f42c1'/%3E%3C/svg%3E")!important}.custom-control-input-purple:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(111,66,193,.25)}.custom-control-input-purple:focus:not(:checked)~.custom-control-label::before{border-color:#b8a2e0}.custom-control-input-purple:not(:disabled):active~.custom-control-label::before{background-color:#d5c8ed;border-color:#d5c8ed}.custom-control-input-pink:checked~.custom-control-label::before{border-color:#e83e8c;background-color:#e83e8c}.custom-control-input-pink.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23e83e8c' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-pink.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23e83e8c'/%3E%3C/svg%3E")!important}.custom-control-input-pink:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(232,62,140,.25)}.custom-control-input-pink:focus:not(:checked)~.custom-control-label::before{border-color:#f6b0d0}.custom-control-input-pink:not(:disabled):active~.custom-control-label::before{background-color:#fbddeb;border-color:#fbddeb}.custom-control-input-red:checked~.custom-control-label::before{border-color:#dc3545;background-color:#dc3545}.custom-control-input-red.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23dc3545' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-red.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23dc3545'/%3E%3C/svg%3E")!important}.custom-control-input-red:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input-red:focus:not(:checked)~.custom-control-label::before{border-color:#efa2a9}.custom-control-input-red:not(:disabled):active~.custom-control-label::before{background-color:#f6cdd1;border-color:#f6cdd1}.custom-control-input-orange:checked~.custom-control-label::before{border-color:#fd7e14;background-color:#fd7e14}.custom-control-input-orange.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fd7e14' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-orange.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fd7e14'/%3E%3C/svg%3E")!important}.custom-control-input-orange:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(253,126,20,.25)}.custom-control-input-orange:focus:not(:checked)~.custom-control-label::before{border-color:#fec392}.custom-control-input-orange:not(:disabled):active~.custom-control-label::before{background-color:#ffdfc5;border-color:#ffdfc5}.custom-control-input-yellow:checked~.custom-control-label::before{border-color:#ffc107;background-color:#ffc107}.custom-control-input-yellow.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffc107' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-yellow.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23ffc107'/%3E%3C/svg%3E")!important}.custom-control-input-yellow:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(255,193,7,.25)}.custom-control-input-yellow:focus:not(:checked)~.custom-control-label::before{border-color:#ffe187}.custom-control-input-yellow:not(:disabled):active~.custom-control-label::before{background-color:#ffeeba;border-color:#ffeeba}.custom-control-input-green:checked~.custom-control-label::before{border-color:#28a745;background-color:#28a745}.custom-control-input-green.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-green.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2328a745'/%3E%3C/svg%3E")!important}.custom-control-input-green:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input-green:focus:not(:checked)~.custom-control-label::before{border-color:#71dd8a}.custom-control-input-green:not(:disabled):active~.custom-control-label::before{background-color:#9be7ac;border-color:#9be7ac}.custom-control-input-teal:checked~.custom-control-label::before{border-color:#20c997;background-color:#20c997}.custom-control-input-teal.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2320c997' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-teal.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2320c997'/%3E%3C/svg%3E")!important}.custom-control-input-teal:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(32,201,151,.25)}.custom-control-input-teal:focus:not(:checked)~.custom-control-label::before{border-color:#7eeaca}.custom-control-input-teal:not(:disabled):active~.custom-control-label::before{background-color:#aaf1dc;border-color:#aaf1dc}.custom-control-input-cyan:checked~.custom-control-label::before{border-color:#17a2b8;background-color:#17a2b8}.custom-control-input-cyan.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2317a2b8' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-cyan.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2317a2b8'/%3E%3C/svg%3E")!important}.custom-control-input-cyan:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(23,162,184,.25)}.custom-control-input-cyan:focus:not(:checked)~.custom-control-label::before{border-color:#63d9ec}.custom-control-input-cyan:not(:disabled):active~.custom-control-label::before{background-color:#90e4f1;border-color:#90e4f1}.custom-control-input-white:checked~.custom-control-label::before{border-color:#fff;background-color:#fff}.custom-control-input-white.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-white.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")!important}.custom-control-input-white:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(255,255,255,.25)}.custom-control-input-white:focus:not(:checked)~.custom-control-label::before{border-color:#fff}.custom-control-input-white:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.custom-control-input-gray:checked~.custom-control-label::before{border-color:#6c757d;background-color:#6c757d}.custom-control-input-gray.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236c757d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-gray.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236c757d'/%3E%3C/svg%3E")!important}.custom-control-input-gray:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(108,117,125,.25)}.custom-control-input-gray:focus:not(:checked)~.custom-control-label::before{border-color:#afb5ba}.custom-control-input-gray:not(:disabled):active~.custom-control-label::before{background-color:#caced1;border-color:#caced1}.custom-control-input-gray-dark:checked~.custom-control-label::before{border-color:#343a40;background-color:#343a40}.custom-control-input-gray-dark.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23343a40' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.custom-control-input-gray-dark.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23343a40'/%3E%3C/svg%3E")!important}.custom-control-input-gray-dark:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(52,58,64,.25)}.custom-control-input-gray-dark:focus:not(:checked)~.custom-control-label::before{border-color:#6d7a86}.custom-control-input-gray-dark:not(:disabled):active~.custom-control-label::before{background-color:#88939e;border-color:#88939e}.custom-control-input-outline~.custom-control-label::before{background-color:transparent!important;box-shadow:none}.custom-control-input-outline:checked~.custom-control-label::before{background-color:transparent}.navbar-dark .btn-navbar,.navbar-dark .form-control-navbar{background-color:#3f474e;border:1px solid #56606a;color:#fff}.navbar-dark .btn-navbar:hover{background-color:#454d55}.navbar-dark .btn-navbar:focus{background-color:#4b545c}.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar,.navbar-dark .form-control-navbar+.input-group-prepend>.btn-navbar{background-color:#3f474e;color:#fff;border:1px solid #56606a;border-left:none}.dark-mode .custom-control-label::before,.dark-mode .custom-file-label,.dark-mode .custom-file-label::after,.dark-mode .custom-select,.dark-mode .form-control:not(.form-control-navbar):not(.form-control-sidebar),.dark-mode .input-group-text{background-color:#343a40;color:#fff}.dark-mode .custom-file-label,.dark-mode .custom-file-label::after,.dark-mode .form-control:not(.form-control-navbar):not(.form-control-sidebar):not(.is-invalid):not(:focus){border-color:#6c757d}.dark-mode select{background-color:#343a40;color:#fff;border-color:#6c757d}.dark-mode .input-group-text{border-color:#6c757d}.dark-mode .custom-control-input:disabled~.custom-control-label::before,.dark-mode .custom-control-input[disabled]~.custom-control-label::before{background-color:#3f474e;border-color:#6c757d;color:#fff}.dark-mode .custom-range::-webkit-slider-runnable-track{background-color:#454d55}.dark-mode .custom-range::-moz-range-track{background-color:#454d55}.dark-mode .custom-range::-ms-track{background-color:#454d55}.dark-mode .custom-range.custom-range-primary:focus{outline:0}.dark-mode .custom-range.custom-range-primary:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-range.custom-range-primary:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-range.custom-range-primary:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-range.custom-range-primary::-webkit-slider-thumb{background-color:#3f6791}.dark-mode .custom-range.custom-range-primary::-webkit-slider-thumb:active{background-color:#a9c1da}.dark-mode .custom-range.custom-range-primary::-moz-range-thumb{background-color:#3f6791}.dark-mode .custom-range.custom-range-primary::-moz-range-thumb:active{background-color:#a9c1da}.dark-mode .custom-range.custom-range-primary::-ms-thumb{background-color:#3f6791}.dark-mode .custom-range.custom-range-primary::-ms-thumb:active{background-color:#a9c1da}.dark-mode .custom-range.custom-range-secondary:focus{outline:0}.dark-mode .custom-range.custom-range-secondary:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-range.custom-range-secondary:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-range.custom-range-secondary:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-range.custom-range-secondary::-webkit-slider-thumb{background-color:#6c757d}.dark-mode .custom-range.custom-range-secondary::-webkit-slider-thumb:active{background-color:#caced1}.dark-mode .custom-range.custom-range-secondary::-moz-range-thumb{background-color:#6c757d}.dark-mode .custom-range.custom-range-secondary::-moz-range-thumb:active{background-color:#caced1}.dark-mode .custom-range.custom-range-secondary::-ms-thumb{background-color:#6c757d}.dark-mode .custom-range.custom-range-secondary::-ms-thumb:active{background-color:#caced1}.dark-mode .custom-range.custom-range-success:focus{outline:0}.dark-mode .custom-range.custom-range-success:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-range.custom-range-success:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-range.custom-range-success:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-range.custom-range-success::-webkit-slider-thumb{background-color:#00bc8c}.dark-mode .custom-range.custom-range-success::-webkit-slider-thumb:active{background-color:#70ffda}.dark-mode .custom-range.custom-range-success::-moz-range-thumb{background-color:#00bc8c}.dark-mode .custom-range.custom-range-success::-moz-range-thumb:active{background-color:#70ffda}.dark-mode .custom-range.custom-range-success::-ms-thumb{background-color:#00bc8c}.dark-mode .custom-range.custom-range-success::-ms-thumb:active{background-color:#70ffda}.dark-mode .custom-range.custom-range-info:focus{outline:0}.dark-mode .custom-range.custom-range-info:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-range.custom-range-info:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-range.custom-range-info:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-range.custom-range-info::-webkit-slider-thumb{background-color:#3498db}.dark-mode .custom-range.custom-range-info::-webkit-slider-thumb:active{background-color:#cce5f6}.dark-mode .custom-range.custom-range-info::-moz-range-thumb{background-color:#3498db}.dark-mode .custom-range.custom-range-info::-moz-range-thumb:active{background-color:#cce5f6}.dark-mode .custom-range.custom-range-info::-ms-thumb{background-color:#3498db}.dark-mode .custom-range.custom-range-info::-ms-thumb:active{background-color:#cce5f6}.dark-mode .custom-range.custom-range-warning:focus{outline:0}.dark-mode .custom-range.custom-range-warning:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-range.custom-range-warning:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-range.custom-range-warning:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-range.custom-range-warning::-webkit-slider-thumb{background-color:#f39c12}.dark-mode .custom-range.custom-range-warning::-webkit-slider-thumb:active{background-color:#fce3bc}.dark-mode .custom-range.custom-range-warning::-moz-range-thumb{background-color:#f39c12}.dark-mode .custom-range.custom-range-warning::-moz-range-thumb:active{background-color:#fce3bc}.dark-mode .custom-range.custom-range-warning::-ms-thumb{background-color:#f39c12}.dark-mode .custom-range.custom-range-warning::-ms-thumb:active{background-color:#fce3bc}.dark-mode .custom-range.custom-range-danger:focus{outline:0}.dark-mode .custom-range.custom-range-danger:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-range.custom-range-danger:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-range.custom-range-danger:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-range.custom-range-danger::-webkit-slider-thumb{background-color:#e74c3c}.dark-mode .custom-range.custom-range-danger::-webkit-slider-thumb:active{background-color:#fbdedb}.dark-mode .custom-range.custom-range-danger::-moz-range-thumb{background-color:#e74c3c}.dark-mode .custom-range.custom-range-danger::-moz-range-thumb:active{background-color:#fbdedb}.dark-mode .custom-range.custom-range-danger::-ms-thumb{background-color:#e74c3c}.dark-mode .custom-range.custom-range-danger::-ms-thumb:active{background-color:#fbdedb}.dark-mode .custom-range.custom-range-light:focus{outline:0}.dark-mode .custom-range.custom-range-light:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.dark-mode .custom-range.custom-range-light:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.dark-mode .custom-range.custom-range-light:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.dark-mode .custom-range.custom-range-light::-webkit-slider-thumb{background-color:#f8f9fa}.dark-mode .custom-range.custom-range-light::-webkit-slider-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-light::-moz-range-thumb{background-color:#f8f9fa}.dark-mode .custom-range.custom-range-light::-moz-range-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-light::-ms-thumb{background-color:#f8f9fa}.dark-mode .custom-range.custom-range-light::-ms-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-dark:focus{outline:0}.dark-mode .custom-range.custom-range-dark:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-range.custom-range-dark:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-range.custom-range-dark:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-range.custom-range-dark::-webkit-slider-thumb{background-color:#343a40}.dark-mode .custom-range.custom-range-dark::-webkit-slider-thumb:active{background-color:#88939e}.dark-mode .custom-range.custom-range-dark::-moz-range-thumb{background-color:#343a40}.dark-mode .custom-range.custom-range-dark::-moz-range-thumb:active{background-color:#88939e}.dark-mode .custom-range.custom-range-dark::-ms-thumb{background-color:#343a40}.dark-mode .custom-range.custom-range-dark::-ms-thumb:active{background-color:#88939e}.dark-mode .custom-range.custom-range-lightblue:focus{outline:0}.dark-mode .custom-range.custom-range-lightblue:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(134,186,216,.25)}.dark-mode .custom-range.custom-range-lightblue:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(134,186,216,.25)}.dark-mode .custom-range.custom-range-lightblue:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(134,186,216,.25)}.dark-mode .custom-range.custom-range-lightblue::-webkit-slider-thumb{background-color:#86bad8}.dark-mode .custom-range.custom-range-lightblue::-webkit-slider-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-lightblue::-moz-range-thumb{background-color:#86bad8}.dark-mode .custom-range.custom-range-lightblue::-moz-range-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-lightblue::-ms-thumb{background-color:#86bad8}.dark-mode .custom-range.custom-range-lightblue::-ms-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-navy:focus{outline:0}.dark-mode .custom-range.custom-range-navy:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,44,89,.25)}.dark-mode .custom-range.custom-range-navy:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,44,89,.25)}.dark-mode .custom-range.custom-range-navy:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,44,89,.25)}.dark-mode .custom-range.custom-range-navy::-webkit-slider-thumb{background-color:#002c59}.dark-mode .custom-range.custom-range-navy::-webkit-slider-thumb:active{background-color:#0c84ff}.dark-mode .custom-range.custom-range-navy::-moz-range-thumb{background-color:#002c59}.dark-mode .custom-range.custom-range-navy::-moz-range-thumb:active{background-color:#0c84ff}.dark-mode .custom-range.custom-range-navy::-ms-thumb{background-color:#002c59}.dark-mode .custom-range.custom-range-navy::-ms-thumb:active{background-color:#0c84ff}.dark-mode .custom-range.custom-range-olive:focus{outline:0}.dark-mode .custom-range.custom-range-olive:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(116,200,163,.25)}.dark-mode .custom-range.custom-range-olive:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(116,200,163,.25)}.dark-mode .custom-range.custom-range-olive:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(116,200,163,.25)}.dark-mode .custom-range.custom-range-olive::-webkit-slider-thumb{background-color:#74c8a3}.dark-mode .custom-range.custom-range-olive::-webkit-slider-thumb:active{background-color:#f4fbf8}.dark-mode .custom-range.custom-range-olive::-moz-range-thumb{background-color:#74c8a3}.dark-mode .custom-range.custom-range-olive::-moz-range-thumb:active{background-color:#f4fbf8}.dark-mode .custom-range.custom-range-olive::-ms-thumb{background-color:#74c8a3}.dark-mode .custom-range.custom-range-olive::-ms-thumb:active{background-color:#f4fbf8}.dark-mode .custom-range.custom-range-lime:focus{outline:0}.dark-mode .custom-range.custom-range-lime:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(103,255,169,.25)}.dark-mode .custom-range.custom-range-lime:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(103,255,169,.25)}.dark-mode .custom-range.custom-range-lime:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(103,255,169,.25)}.dark-mode .custom-range.custom-range-lime::-webkit-slider-thumb{background-color:#67ffa9}.dark-mode .custom-range.custom-range-lime::-webkit-slider-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-lime::-moz-range-thumb{background-color:#67ffa9}.dark-mode .custom-range.custom-range-lime::-moz-range-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-lime::-ms-thumb{background-color:#67ffa9}.dark-mode .custom-range.custom-range-lime::-ms-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-fuchsia:focus{outline:0}.dark-mode .custom-range.custom-range-fuchsia:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(246,114,216,.25)}.dark-mode .custom-range.custom-range-fuchsia:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(246,114,216,.25)}.dark-mode .custom-range.custom-range-fuchsia:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(246,114,216,.25)}.dark-mode .custom-range.custom-range-fuchsia::-webkit-slider-thumb{background-color:#f672d8}.dark-mode .custom-range.custom-range-fuchsia::-webkit-slider-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-fuchsia::-moz-range-thumb{background-color:#f672d8}.dark-mode .custom-range.custom-range-fuchsia::-moz-range-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-fuchsia::-ms-thumb{background-color:#f672d8}.dark-mode .custom-range.custom-range-fuchsia::-ms-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-maroon:focus{outline:0}.dark-mode .custom-range.custom-range-maroon:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(237,108,155,.25)}.dark-mode .custom-range.custom-range-maroon:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(237,108,155,.25)}.dark-mode .custom-range.custom-range-maroon:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(237,108,155,.25)}.dark-mode .custom-range.custom-range-maroon::-webkit-slider-thumb{background-color:#ed6c9b}.dark-mode .custom-range.custom-range-maroon::-webkit-slider-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-maroon::-moz-range-thumb{background-color:#ed6c9b}.dark-mode .custom-range.custom-range-maroon::-moz-range-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-maroon::-ms-thumb{background-color:#ed6c9b}.dark-mode .custom-range.custom-range-maroon::-ms-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-blue:focus{outline:0}.dark-mode .custom-range.custom-range-blue:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-range.custom-range-blue:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-range.custom-range-blue:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-range.custom-range-blue::-webkit-slider-thumb{background-color:#3f6791}.dark-mode .custom-range.custom-range-blue::-webkit-slider-thumb:active{background-color:#a9c1da}.dark-mode .custom-range.custom-range-blue::-moz-range-thumb{background-color:#3f6791}.dark-mode .custom-range.custom-range-blue::-moz-range-thumb:active{background-color:#a9c1da}.dark-mode .custom-range.custom-range-blue::-ms-thumb{background-color:#3f6791}.dark-mode .custom-range.custom-range-blue::-ms-thumb:active{background-color:#a9c1da}.dark-mode .custom-range.custom-range-indigo:focus{outline:0}.dark-mode .custom-range.custom-range-indigo:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.dark-mode .custom-range.custom-range-indigo:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.dark-mode .custom-range.custom-range-indigo:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.dark-mode .custom-range.custom-range-indigo::-webkit-slider-thumb{background-color:#6610f2}.dark-mode .custom-range.custom-range-indigo::-webkit-slider-thumb:active{background-color:#d2b9fb}.dark-mode .custom-range.custom-range-indigo::-moz-range-thumb{background-color:#6610f2}.dark-mode .custom-range.custom-range-indigo::-moz-range-thumb:active{background-color:#d2b9fb}.dark-mode .custom-range.custom-range-indigo::-ms-thumb{background-color:#6610f2}.dark-mode .custom-range.custom-range-indigo::-ms-thumb:active{background-color:#d2b9fb}.dark-mode .custom-range.custom-range-purple:focus{outline:0}.dark-mode .custom-range.custom-range-purple:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.dark-mode .custom-range.custom-range-purple:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.dark-mode .custom-range.custom-range-purple:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.dark-mode .custom-range.custom-range-purple::-webkit-slider-thumb{background-color:#6f42c1}.dark-mode .custom-range.custom-range-purple::-webkit-slider-thumb:active{background-color:#d5c8ed}.dark-mode .custom-range.custom-range-purple::-moz-range-thumb{background-color:#6f42c1}.dark-mode .custom-range.custom-range-purple::-moz-range-thumb:active{background-color:#d5c8ed}.dark-mode .custom-range.custom-range-purple::-ms-thumb{background-color:#6f42c1}.dark-mode .custom-range.custom-range-purple::-ms-thumb:active{background-color:#d5c8ed}.dark-mode .custom-range.custom-range-pink:focus{outline:0}.dark-mode .custom-range.custom-range-pink:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.dark-mode .custom-range.custom-range-pink:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.dark-mode .custom-range.custom-range-pink:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.dark-mode .custom-range.custom-range-pink::-webkit-slider-thumb{background-color:#e83e8c}.dark-mode .custom-range.custom-range-pink::-webkit-slider-thumb:active{background-color:#fbddeb}.dark-mode .custom-range.custom-range-pink::-moz-range-thumb{background-color:#e83e8c}.dark-mode .custom-range.custom-range-pink::-moz-range-thumb:active{background-color:#fbddeb}.dark-mode .custom-range.custom-range-pink::-ms-thumb{background-color:#e83e8c}.dark-mode .custom-range.custom-range-pink::-ms-thumb:active{background-color:#fbddeb}.dark-mode .custom-range.custom-range-red:focus{outline:0}.dark-mode .custom-range.custom-range-red:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-range.custom-range-red:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-range.custom-range-red:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-range.custom-range-red::-webkit-slider-thumb{background-color:#e74c3c}.dark-mode .custom-range.custom-range-red::-webkit-slider-thumb:active{background-color:#fbdedb}.dark-mode .custom-range.custom-range-red::-moz-range-thumb{background-color:#e74c3c}.dark-mode .custom-range.custom-range-red::-moz-range-thumb:active{background-color:#fbdedb}.dark-mode .custom-range.custom-range-red::-ms-thumb{background-color:#e74c3c}.dark-mode .custom-range.custom-range-red::-ms-thumb:active{background-color:#fbdedb}.dark-mode .custom-range.custom-range-orange:focus{outline:0}.dark-mode .custom-range.custom-range-orange:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.dark-mode .custom-range.custom-range-orange:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.dark-mode .custom-range.custom-range-orange:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.dark-mode .custom-range.custom-range-orange::-webkit-slider-thumb{background-color:#fd7e14}.dark-mode .custom-range.custom-range-orange::-webkit-slider-thumb:active{background-color:#ffdfc5}.dark-mode .custom-range.custom-range-orange::-moz-range-thumb{background-color:#fd7e14}.dark-mode .custom-range.custom-range-orange::-moz-range-thumb:active{background-color:#ffdfc5}.dark-mode .custom-range.custom-range-orange::-ms-thumb{background-color:#fd7e14}.dark-mode .custom-range.custom-range-orange::-ms-thumb:active{background-color:#ffdfc5}.dark-mode .custom-range.custom-range-yellow:focus{outline:0}.dark-mode .custom-range.custom-range-yellow:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-range.custom-range-yellow:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-range.custom-range-yellow:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-range.custom-range-yellow::-webkit-slider-thumb{background-color:#f39c12}.dark-mode .custom-range.custom-range-yellow::-webkit-slider-thumb:active{background-color:#fce3bc}.dark-mode .custom-range.custom-range-yellow::-moz-range-thumb{background-color:#f39c12}.dark-mode .custom-range.custom-range-yellow::-moz-range-thumb:active{background-color:#fce3bc}.dark-mode .custom-range.custom-range-yellow::-ms-thumb{background-color:#f39c12}.dark-mode .custom-range.custom-range-yellow::-ms-thumb:active{background-color:#fce3bc}.dark-mode .custom-range.custom-range-green:focus{outline:0}.dark-mode .custom-range.custom-range-green:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-range.custom-range-green:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-range.custom-range-green:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-range.custom-range-green::-webkit-slider-thumb{background-color:#00bc8c}.dark-mode .custom-range.custom-range-green::-webkit-slider-thumb:active{background-color:#70ffda}.dark-mode .custom-range.custom-range-green::-moz-range-thumb{background-color:#00bc8c}.dark-mode .custom-range.custom-range-green::-moz-range-thumb:active{background-color:#70ffda}.dark-mode .custom-range.custom-range-green::-ms-thumb{background-color:#00bc8c}.dark-mode .custom-range.custom-range-green::-ms-thumb:active{background-color:#70ffda}.dark-mode .custom-range.custom-range-teal:focus{outline:0}.dark-mode .custom-range.custom-range-teal:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.dark-mode .custom-range.custom-range-teal:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.dark-mode .custom-range.custom-range-teal:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.dark-mode .custom-range.custom-range-teal::-webkit-slider-thumb{background-color:#20c997}.dark-mode .custom-range.custom-range-teal::-webkit-slider-thumb:active{background-color:#aaf1dc}.dark-mode .custom-range.custom-range-teal::-moz-range-thumb{background-color:#20c997}.dark-mode .custom-range.custom-range-teal::-moz-range-thumb:active{background-color:#aaf1dc}.dark-mode .custom-range.custom-range-teal::-ms-thumb{background-color:#20c997}.dark-mode .custom-range.custom-range-teal::-ms-thumb:active{background-color:#aaf1dc}.dark-mode .custom-range.custom-range-cyan:focus{outline:0}.dark-mode .custom-range.custom-range-cyan:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-range.custom-range-cyan:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-range.custom-range-cyan:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-range.custom-range-cyan::-webkit-slider-thumb{background-color:#3498db}.dark-mode .custom-range.custom-range-cyan::-webkit-slider-thumb:active{background-color:#cce5f6}.dark-mode .custom-range.custom-range-cyan::-moz-range-thumb{background-color:#3498db}.dark-mode .custom-range.custom-range-cyan::-moz-range-thumb:active{background-color:#cce5f6}.dark-mode .custom-range.custom-range-cyan::-ms-thumb{background-color:#3498db}.dark-mode .custom-range.custom-range-cyan::-ms-thumb:active{background-color:#cce5f6}.dark-mode .custom-range.custom-range-white:focus{outline:0}.dark-mode .custom-range.custom-range-white:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.dark-mode .custom-range.custom-range-white:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.dark-mode .custom-range.custom-range-white:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.dark-mode .custom-range.custom-range-white::-webkit-slider-thumb{background-color:#fff}.dark-mode .custom-range.custom-range-white::-webkit-slider-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-white::-moz-range-thumb{background-color:#fff}.dark-mode .custom-range.custom-range-white::-moz-range-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-white::-ms-thumb{background-color:#fff}.dark-mode .custom-range.custom-range-white::-ms-thumb:active{background-color:#fff}.dark-mode .custom-range.custom-range-gray:focus{outline:0}.dark-mode .custom-range.custom-range-gray:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-range.custom-range-gray:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-range.custom-range-gray:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-range.custom-range-gray::-webkit-slider-thumb{background-color:#6c757d}.dark-mode .custom-range.custom-range-gray::-webkit-slider-thumb:active{background-color:#caced1}.dark-mode .custom-range.custom-range-gray::-moz-range-thumb{background-color:#6c757d}.dark-mode .custom-range.custom-range-gray::-moz-range-thumb:active{background-color:#caced1}.dark-mode .custom-range.custom-range-gray::-ms-thumb{background-color:#6c757d}.dark-mode .custom-range.custom-range-gray::-ms-thumb:active{background-color:#caced1}.dark-mode .custom-range.custom-range-gray-dark:focus{outline:0}.dark-mode .custom-range.custom-range-gray-dark:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-range.custom-range-gray-dark:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-range.custom-range-gray-dark:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-range.custom-range-gray-dark::-webkit-slider-thumb{background-color:#343a40}.dark-mode .custom-range.custom-range-gray-dark::-webkit-slider-thumb:active{background-color:#88939e}.dark-mode .custom-range.custom-range-gray-dark::-moz-range-thumb{background-color:#343a40}.dark-mode .custom-range.custom-range-gray-dark::-moz-range-thumb:active{background-color:#88939e}.dark-mode .custom-range.custom-range-gray-dark::-ms-thumb{background-color:#343a40}.dark-mode .custom-range.custom-range-gray-dark::-ms-thumb:active{background-color:#88939e}.dark-mode .custom-switch.custom-switch-off-primary .custom-control-input~.custom-control-label::before{background-color:#3f6791;border-color:#20344a}.dark-mode .custom-switch.custom-switch-off-primary .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-switch.custom-switch-off-primary .custom-control-input~.custom-control-label::after{background-color:#182838}.dark-mode .custom-switch.custom-switch-on-primary .custom-control-input:checked~.custom-control-label::before{background-color:#3f6791;border-color:#20344a}.dark-mode .custom-switch.custom-switch-on-primary .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-switch.custom-switch-on-primary .custom-control-input:checked~.custom-control-label::after{background-color:#97b4d2}.dark-mode .custom-switch.custom-switch-off-secondary .custom-control-input~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.dark-mode .custom-switch.custom-switch-off-secondary .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-switch.custom-switch-off-secondary .custom-control-input~.custom-control-label::after{background-color:#313539}.dark-mode .custom-switch.custom-switch-on-secondary .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.dark-mode .custom-switch.custom-switch-on-secondary .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-switch.custom-switch-on-secondary .custom-control-input:checked~.custom-control-label::after{background-color:#bcc1c6}.dark-mode .custom-switch.custom-switch-off-success .custom-control-input~.custom-control-label::before{background-color:#00bc8c;border-color:#005640}.dark-mode .custom-switch.custom-switch-off-success .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-switch.custom-switch-off-success .custom-control-input~.custom-control-label::after{background-color:#003d2d}.dark-mode .custom-switch.custom-switch-on-success .custom-control-input:checked~.custom-control-label::before{background-color:#00bc8c;border-color:#005640}.dark-mode .custom-switch.custom-switch-on-success .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-switch.custom-switch-on-success .custom-control-input:checked~.custom-control-label::after{background-color:#56ffd4}.dark-mode .custom-switch.custom-switch-off-info .custom-control-input~.custom-control-label::before{background-color:#3498db;border-color:#196090}.dark-mode .custom-switch.custom-switch-off-info .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-switch.custom-switch-off-info .custom-control-input~.custom-control-label::after{background-color:#16527a}.dark-mode .custom-switch.custom-switch-on-info .custom-control-input:checked~.custom-control-label::before{background-color:#3498db;border-color:#196090}.dark-mode .custom-switch.custom-switch-on-info .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-switch.custom-switch-on-info .custom-control-input:checked~.custom-control-label::after{background-color:#b6daf2}.dark-mode .custom-switch.custom-switch-off-warning .custom-control-input~.custom-control-label::before{background-color:#f39c12;border-color:#976008}.dark-mode .custom-switch.custom-switch-off-warning .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-switch.custom-switch-off-warning .custom-control-input~.custom-control-label::after{background-color:#7f5006}.dark-mode .custom-switch.custom-switch-on-warning .custom-control-input:checked~.custom-control-label::before{background-color:#f39c12;border-color:#976008}.dark-mode .custom-switch.custom-switch-on-warning .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-switch.custom-switch-on-warning .custom-control-input:checked~.custom-control-label::after{background-color:#fad9a4}.dark-mode .custom-switch.custom-switch-off-danger .custom-control-input~.custom-control-label::before{background-color:#e74c3c;border-color:#a82315}.dark-mode .custom-switch.custom-switch-off-danger .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-switch.custom-switch-off-danger .custom-control-input~.custom-control-label::after{background-color:#921e12}.dark-mode .custom-switch.custom-switch-on-danger .custom-control-input:checked~.custom-control-label::before{background-color:#e74c3c;border-color:#a82315}.dark-mode .custom-switch.custom-switch-on-danger .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-switch.custom-switch-on-danger .custom-control-input:checked~.custom-control-label::after{background-color:#f8c9c4}.dark-mode .custom-switch.custom-switch-off-light .custom-control-input~.custom-control-label::before{background-color:#f8f9fa;border-color:#bdc6d0}.dark-mode .custom-switch.custom-switch-off-light .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.dark-mode .custom-switch.custom-switch-off-light .custom-control-input~.custom-control-label::after{background-color:#aeb9c5}.dark-mode .custom-switch.custom-switch-on-light .custom-control-input:checked~.custom-control-label::before{background-color:#f8f9fa;border-color:#bdc6d0}.dark-mode .custom-switch.custom-switch-on-light .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.dark-mode .custom-switch.custom-switch-on-light .custom-control-input:checked~.custom-control-label::after{background-color:#fff}.dark-mode .custom-switch.custom-switch-off-dark .custom-control-input~.custom-control-label::before{background-color:#343a40;border-color:#060708}.dark-mode .custom-switch.custom-switch-off-dark .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-switch.custom-switch-off-dark .custom-control-input~.custom-control-label::after{background-color:#000}.dark-mode .custom-switch.custom-switch-on-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.dark-mode .custom-switch.custom-switch-on-dark .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-switch.custom-switch-on-dark .custom-control-input:checked~.custom-control-label::after{background-color:#7a8793}.dark-mode .custom-switch.custom-switch-off-lightblue .custom-control-input~.custom-control-label::before{background-color:#86bad8;border-color:#3c8dbc}.dark-mode .custom-switch.custom-switch-off-lightblue .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(134,186,216,.25)}.dark-mode .custom-switch.custom-switch-off-lightblue .custom-control-input~.custom-control-label::after{background-color:#367fa9}.dark-mode .custom-switch.custom-switch-on-lightblue .custom-control-input:checked~.custom-control-label::before{background-color:#86bad8;border-color:#3c8dbc}.dark-mode .custom-switch.custom-switch-on-lightblue .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(134,186,216,.25)}.dark-mode .custom-switch.custom-switch-on-lightblue .custom-control-input:checked~.custom-control-label::after{background-color:#fafcfd}.dark-mode .custom-switch.custom-switch-off-navy .custom-control-input~.custom-control-label::before{background-color:#002c59;border-color:#000}.dark-mode .custom-switch.custom-switch-off-navy .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,44,89,.25)}.dark-mode .custom-switch.custom-switch-off-navy .custom-control-input~.custom-control-label::after{background-color:#000}.dark-mode .custom-switch.custom-switch-on-navy .custom-control-input:checked~.custom-control-label::before{background-color:#002c59;border-color:#000}.dark-mode .custom-switch.custom-switch-on-navy .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,44,89,.25)}.dark-mode .custom-switch.custom-switch-on-navy .custom-control-input:checked~.custom-control-label::after{background-color:#0077f2}.dark-mode .custom-switch.custom-switch-off-olive .custom-control-input~.custom-control-label::before{background-color:#74c8a3;border-color:#3d9970}.dark-mode .custom-switch.custom-switch-off-olive .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(116,200,163,.25)}.dark-mode .custom-switch.custom-switch-off-olive .custom-control-input~.custom-control-label::after{background-color:#368763}.dark-mode .custom-switch.custom-switch-on-olive .custom-control-input:checked~.custom-control-label::before{background-color:#74c8a3;border-color:#3d9970}.dark-mode .custom-switch.custom-switch-on-olive .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(116,200,163,.25)}.dark-mode .custom-switch.custom-switch-on-olive .custom-control-input:checked~.custom-control-label::after{background-color:#e2f3eb}.dark-mode .custom-switch.custom-switch-off-lime .custom-control-input~.custom-control-label::before{background-color:#67ffa9;border-color:#01ff70}.dark-mode .custom-switch.custom-switch-off-lime .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(103,255,169,.25)}.dark-mode .custom-switch.custom-switch-off-lime .custom-control-input~.custom-control-label::after{background-color:#00e765}.dark-mode .custom-switch.custom-switch-on-lime .custom-control-input:checked~.custom-control-label::before{background-color:#67ffa9;border-color:#01ff70}.dark-mode .custom-switch.custom-switch-on-lime .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(103,255,169,.25)}.dark-mode .custom-switch.custom-switch-on-lime .custom-control-input:checked~.custom-control-label::after{background-color:#fff}.dark-mode .custom-switch.custom-switch-off-fuchsia .custom-control-input~.custom-control-label::before{background-color:#f672d8;border-color:#f012be}.dark-mode .custom-switch.custom-switch-off-fuchsia .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(246,114,216,.25)}.dark-mode .custom-switch.custom-switch-off-fuchsia .custom-control-input~.custom-control-label::after{background-color:#db0ead}.dark-mode .custom-switch.custom-switch-on-fuchsia .custom-control-input:checked~.custom-control-label::before{background-color:#f672d8;border-color:#f012be}.dark-mode .custom-switch.custom-switch-on-fuchsia .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(246,114,216,.25)}.dark-mode .custom-switch.custom-switch-on-fuchsia .custom-control-input:checked~.custom-control-label::after{background-color:#fff}.dark-mode .custom-switch.custom-switch-off-maroon .custom-control-input~.custom-control-label::before{background-color:#ed6c9b;border-color:#d81b60}.dark-mode .custom-switch.custom-switch-off-maroon .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(237,108,155,.25)}.dark-mode .custom-switch.custom-switch-off-maroon .custom-control-input~.custom-control-label::after{background-color:#c11856}.dark-mode .custom-switch.custom-switch-on-maroon .custom-control-input:checked~.custom-control-label::before{background-color:#ed6c9b;border-color:#d81b60}.dark-mode .custom-switch.custom-switch-on-maroon .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(237,108,155,.25)}.dark-mode .custom-switch.custom-switch-on-maroon .custom-control-input:checked~.custom-control-label::after{background-color:#fef4f8}.dark-mode .custom-switch.custom-switch-off-blue .custom-control-input~.custom-control-label::before{background-color:#3f6791;border-color:#20344a}.dark-mode .custom-switch.custom-switch-off-blue .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-switch.custom-switch-off-blue .custom-control-input~.custom-control-label::after{background-color:#182838}.dark-mode .custom-switch.custom-switch-on-blue .custom-control-input:checked~.custom-control-label::before{background-color:#3f6791;border-color:#20344a}.dark-mode .custom-switch.custom-switch-on-blue .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(63,103,145,.25)}.dark-mode .custom-switch.custom-switch-on-blue .custom-control-input:checked~.custom-control-label::after{background-color:#97b4d2}.dark-mode .custom-switch.custom-switch-off-indigo .custom-control-input~.custom-control-label::before{background-color:#6610f2;border-color:#3d0894}.dark-mode .custom-switch.custom-switch-off-indigo .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.dark-mode .custom-switch.custom-switch-off-indigo .custom-control-input~.custom-control-label::after{background-color:#33077c}.dark-mode .custom-switch.custom-switch-on-indigo .custom-control-input:checked~.custom-control-label::before{background-color:#6610f2;border-color:#3d0894}.dark-mode .custom-switch.custom-switch-on-indigo .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.dark-mode .custom-switch.custom-switch-on-indigo .custom-control-input:checked~.custom-control-label::after{background-color:#c3a1fa}.dark-mode .custom-switch.custom-switch-off-purple .custom-control-input~.custom-control-label::before{background-color:#6f42c1;border-color:#432776}.dark-mode .custom-switch.custom-switch-off-purple .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.dark-mode .custom-switch.custom-switch-off-purple .custom-control-input~.custom-control-label::after{background-color:#382063}.dark-mode .custom-switch.custom-switch-on-purple .custom-control-input:checked~.custom-control-label::before{background-color:#6f42c1;border-color:#432776}.dark-mode .custom-switch.custom-switch-on-purple .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.dark-mode .custom-switch.custom-switch-on-purple .custom-control-input:checked~.custom-control-label::after{background-color:#c7b5e7}.dark-mode .custom-switch.custom-switch-off-pink .custom-control-input~.custom-control-label::before{background-color:#e83e8c;border-color:#ac145a}.dark-mode .custom-switch.custom-switch-off-pink .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.dark-mode .custom-switch.custom-switch-off-pink .custom-control-input~.custom-control-label::after{background-color:#95124e}.dark-mode .custom-switch.custom-switch-on-pink .custom-control-input:checked~.custom-control-label::before{background-color:#e83e8c;border-color:#ac145a}.dark-mode .custom-switch.custom-switch-on-pink .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.dark-mode .custom-switch.custom-switch-on-pink .custom-control-input:checked~.custom-control-label::after{background-color:#f8c7dd}.dark-mode .custom-switch.custom-switch-off-red .custom-control-input~.custom-control-label::before{background-color:#e74c3c;border-color:#a82315}.dark-mode .custom-switch.custom-switch-off-red .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-switch.custom-switch-off-red .custom-control-input~.custom-control-label::after{background-color:#921e12}.dark-mode .custom-switch.custom-switch-on-red .custom-control-input:checked~.custom-control-label::before{background-color:#e74c3c;border-color:#a82315}.dark-mode .custom-switch.custom-switch-on-red .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(231,76,60,.25)}.dark-mode .custom-switch.custom-switch-on-red .custom-control-input:checked~.custom-control-label::after{background-color:#f8c9c4}.dark-mode .custom-switch.custom-switch-off-orange .custom-control-input~.custom-control-label::before{background-color:#fd7e14;border-color:#aa4e01}.dark-mode .custom-switch.custom-switch-off-orange .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.dark-mode .custom-switch.custom-switch-off-orange .custom-control-input~.custom-control-label::after{background-color:#904201}.dark-mode .custom-switch.custom-switch-on-orange .custom-control-input:checked~.custom-control-label::before{background-color:#fd7e14;border-color:#aa4e01}.dark-mode .custom-switch.custom-switch-on-orange .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.dark-mode .custom-switch.custom-switch-on-orange .custom-control-input:checked~.custom-control-label::after{background-color:#fed1ac}.dark-mode .custom-switch.custom-switch-off-yellow .custom-control-input~.custom-control-label::before{background-color:#f39c12;border-color:#976008}.dark-mode .custom-switch.custom-switch-off-yellow .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-switch.custom-switch-off-yellow .custom-control-input~.custom-control-label::after{background-color:#7f5006}.dark-mode .custom-switch.custom-switch-on-yellow .custom-control-input:checked~.custom-control-label::before{background-color:#f39c12;border-color:#976008}.dark-mode .custom-switch.custom-switch-on-yellow .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(243,156,18,.25)}.dark-mode .custom-switch.custom-switch-on-yellow .custom-control-input:checked~.custom-control-label::after{background-color:#fad9a4}.dark-mode .custom-switch.custom-switch-off-green .custom-control-input~.custom-control-label::before{background-color:#00bc8c;border-color:#005640}.dark-mode .custom-switch.custom-switch-off-green .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-switch.custom-switch-off-green .custom-control-input~.custom-control-label::after{background-color:#003d2d}.dark-mode .custom-switch.custom-switch-on-green .custom-control-input:checked~.custom-control-label::before{background-color:#00bc8c;border-color:#005640}.dark-mode .custom-switch.custom-switch-on-green .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,188,140,.25)}.dark-mode .custom-switch.custom-switch-on-green .custom-control-input:checked~.custom-control-label::after{background-color:#56ffd4}.dark-mode .custom-switch.custom-switch-off-teal .custom-control-input~.custom-control-label::before{background-color:#20c997;border-color:#127155}.dark-mode .custom-switch.custom-switch-off-teal .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.dark-mode .custom-switch.custom-switch-off-teal .custom-control-input~.custom-control-label::after{background-color:#0e5b44}.dark-mode .custom-switch.custom-switch-on-teal .custom-control-input:checked~.custom-control-label::before{background-color:#20c997;border-color:#127155}.dark-mode .custom-switch.custom-switch-on-teal .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.dark-mode .custom-switch.custom-switch-on-teal .custom-control-input:checked~.custom-control-label::after{background-color:#94eed3}.dark-mode .custom-switch.custom-switch-off-cyan .custom-control-input~.custom-control-label::before{background-color:#3498db;border-color:#196090}.dark-mode .custom-switch.custom-switch-off-cyan .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-switch.custom-switch-off-cyan .custom-control-input~.custom-control-label::after{background-color:#16527a}.dark-mode .custom-switch.custom-switch-on-cyan .custom-control-input:checked~.custom-control-label::before{background-color:#3498db;border-color:#196090}.dark-mode .custom-switch.custom-switch-on-cyan .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,152,219,.25)}.dark-mode .custom-switch.custom-switch-on-cyan .custom-control-input:checked~.custom-control-label::after{background-color:#b6daf2}.dark-mode .custom-switch.custom-switch-off-white .custom-control-input~.custom-control-label::before{background-color:#fff;border-color:#ccc}.dark-mode .custom-switch.custom-switch-off-white .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.dark-mode .custom-switch.custom-switch-off-white .custom-control-input~.custom-control-label::after{background-color:#bfbfbf}.dark-mode .custom-switch.custom-switch-on-white .custom-control-input:checked~.custom-control-label::before{background-color:#fff;border-color:#ccc}.dark-mode .custom-switch.custom-switch-on-white .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.dark-mode .custom-switch.custom-switch-on-white .custom-control-input:checked~.custom-control-label::after{background-color:#fff}.dark-mode .custom-switch.custom-switch-off-gray .custom-control-input~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.dark-mode .custom-switch.custom-switch-off-gray .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-switch.custom-switch-off-gray .custom-control-input~.custom-control-label::after{background-color:#313539}.dark-mode .custom-switch.custom-switch-on-gray .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.dark-mode .custom-switch.custom-switch-on-gray .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.dark-mode .custom-switch.custom-switch-on-gray .custom-control-input:checked~.custom-control-label::after{background-color:#bcc1c6}.dark-mode .custom-switch.custom-switch-off-gray-dark .custom-control-input~.custom-control-label::before{background-color:#343a40;border-color:#060708}.dark-mode .custom-switch.custom-switch-off-gray-dark .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-switch.custom-switch-off-gray-dark .custom-control-input~.custom-control-label::after{background-color:#000}.dark-mode .custom-switch.custom-switch-on-gray-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.dark-mode .custom-switch.custom-switch-on-gray-dark .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.dark-mode .custom-switch.custom-switch-on-gray-dark .custom-control-input:checked~.custom-control-label::after{background-color:#7a8793}.dark-mode .custom-control-input-primary:checked~.custom-control-label::before{border-color:#3f6791;background-color:#3f6791}.dark-mode .custom-control-input-primary.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%233f6791' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-primary.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%233f6791'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-primary:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(63,103,145,.25)}.dark-mode .custom-control-input-primary:focus:not(:checked)~.custom-control-label::before{border-color:#85a7ca}.dark-mode .custom-control-input-primary:not(:disabled):active~.custom-control-label::before{background-color:#a9c1da;border-color:#a9c1da}.dark-mode .custom-control-input-secondary:checked~.custom-control-label::before{border-color:#6c757d;background-color:#6c757d}.dark-mode .custom-control-input-secondary.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236c757d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-secondary.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236c757d'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-secondary:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(108,117,125,.25)}.dark-mode .custom-control-input-secondary:focus:not(:checked)~.custom-control-label::before{border-color:#afb5ba}.dark-mode .custom-control-input-secondary:not(:disabled):active~.custom-control-label::before{background-color:#caced1;border-color:#caced1}.dark-mode .custom-control-input-success:checked~.custom-control-label::before{border-color:#00bc8c;background-color:#00bc8c}.dark-mode .custom-control-input-success.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2300bc8c' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-success.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2300bc8c'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-success:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(0,188,140,.25)}.dark-mode .custom-control-input-success:focus:not(:checked)~.custom-control-label::before{border-color:#3dffcd}.dark-mode .custom-control-input-success:not(:disabled):active~.custom-control-label::before{background-color:#70ffda;border-color:#70ffda}.dark-mode .custom-control-input-info:checked~.custom-control-label::before{border-color:#3498db;background-color:#3498db}.dark-mode .custom-control-input-info.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%233498db' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-info.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%233498db'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-info:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(52,152,219,.25)}.dark-mode .custom-control-input-info:focus:not(:checked)~.custom-control-label::before{border-color:#a0cfee}.dark-mode .custom-control-input-info:not(:disabled):active~.custom-control-label::before{background-color:#cce5f6;border-color:#cce5f6}.dark-mode .custom-control-input-warning:checked~.custom-control-label::before{border-color:#f39c12;background-color:#f39c12}.dark-mode .custom-control-input-warning.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f39c12' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-warning.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23f39c12'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-warning:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(243,156,18,.25)}.dark-mode .custom-control-input-warning:focus:not(:checked)~.custom-control-label::before{border-color:#f9cf8b}.dark-mode .custom-control-input-warning:not(:disabled):active~.custom-control-label::before{background-color:#fce3bc;border-color:#fce3bc}.dark-mode .custom-control-input-danger:checked~.custom-control-label::before{border-color:#e74c3c;background-color:#e74c3c}.dark-mode .custom-control-input-danger.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23e74c3c' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-danger.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23e74c3c'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-danger:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(231,76,60,.25)}.dark-mode .custom-control-input-danger:focus:not(:checked)~.custom-control-label::before{border-color:#f5b4ae}.dark-mode .custom-control-input-danger:not(:disabled):active~.custom-control-label::before{background-color:#fbdedb;border-color:#fbdedb}.dark-mode .custom-control-input-light:checked~.custom-control-label::before{border-color:#f8f9fa;background-color:#f8f9fa}.dark-mode .custom-control-input-light.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f8f9fa' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-light.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23f8f9fa'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-light:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(248,249,250,.25)}.dark-mode .custom-control-input-light:focus:not(:checked)~.custom-control-label::before{border-color:#fff}.dark-mode .custom-control-input-light:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.dark-mode .custom-control-input-dark:checked~.custom-control-label::before{border-color:#343a40;background-color:#343a40}.dark-mode .custom-control-input-dark.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23343a40' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-dark.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23343a40'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-dark:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(52,58,64,.25)}.dark-mode .custom-control-input-dark:focus:not(:checked)~.custom-control-label::before{border-color:#6d7a86}.dark-mode .custom-control-input-dark:not(:disabled):active~.custom-control-label::before{background-color:#88939e;border-color:#88939e}.dark-mode .custom-control-input-lightblue:checked~.custom-control-label::before{border-color:#86bad8;background-color:#86bad8}.dark-mode .custom-control-input-lightblue.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2386bad8' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-lightblue.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2386bad8'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-lightblue:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(134,186,216,.25)}.dark-mode .custom-control-input-lightblue:focus:not(:checked)~.custom-control-label::before{border-color:#e6f1f7}.dark-mode .custom-control-input-lightblue:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.dark-mode .custom-control-input-navy:checked~.custom-control-label::before{border-color:#002c59;background-color:#002c59}.dark-mode .custom-control-input-navy.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23002c59' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-navy.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23002c59'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-navy:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(0,44,89,.25)}.dark-mode .custom-control-input-navy:focus:not(:checked)~.custom-control-label::before{border-color:#006ad8}.dark-mode .custom-control-input-navy:not(:disabled):active~.custom-control-label::before{background-color:#0c84ff;border-color:#0c84ff}.dark-mode .custom-control-input-olive:checked~.custom-control-label::before{border-color:#74c8a3;background-color:#74c8a3}.dark-mode .custom-control-input-olive.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2374c8a3' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-olive.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2374c8a3'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-olive:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(116,200,163,.25)}.dark-mode .custom-control-input-olive:focus:not(:checked)~.custom-control-label::before{border-color:#cfecdf}.dark-mode .custom-control-input-olive:not(:disabled):active~.custom-control-label::before{background-color:#f4fbf8;border-color:#f4fbf8}.dark-mode .custom-control-input-lime:checked~.custom-control-label::before{border-color:#67ffa9;background-color:#67ffa9}.dark-mode .custom-control-input-lime.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2367ffa9' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-lime.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2367ffa9'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-lime:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(103,255,169,.25)}.dark-mode .custom-control-input-lime:focus:not(:checked)~.custom-control-label::before{border-color:#e7fff1}.dark-mode .custom-control-input-lime:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.dark-mode .custom-control-input-fuchsia:checked~.custom-control-label::before{border-color:#f672d8;background-color:#f672d8}.dark-mode .custom-control-input-fuchsia.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f672d8' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-fuchsia.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23f672d8'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-fuchsia:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(246,114,216,.25)}.dark-mode .custom-control-input-fuchsia:focus:not(:checked)~.custom-control-label::before{border-color:#feeaf9}.dark-mode .custom-control-input-fuchsia:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.dark-mode .custom-control-input-maroon:checked~.custom-control-label::before{border-color:#ed6c9b;background-color:#ed6c9b}.dark-mode .custom-control-input-maroon.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ed6c9b' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-maroon.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23ed6c9b'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-maroon:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(237,108,155,.25)}.dark-mode .custom-control-input-maroon:focus:not(:checked)~.custom-control-label::before{border-color:#fbdee8}.dark-mode .custom-control-input-maroon:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.dark-mode .custom-control-input-blue:checked~.custom-control-label::before{border-color:#3f6791;background-color:#3f6791}.dark-mode .custom-control-input-blue.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%233f6791' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-blue.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%233f6791'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-blue:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(63,103,145,.25)}.dark-mode .custom-control-input-blue:focus:not(:checked)~.custom-control-label::before{border-color:#85a7ca}.dark-mode .custom-control-input-blue:not(:disabled):active~.custom-control-label::before{background-color:#a9c1da;border-color:#a9c1da}.dark-mode .custom-control-input-indigo:checked~.custom-control-label::before{border-color:#6610f2;background-color:#6610f2}.dark-mode .custom-control-input-indigo.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236610f2' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-indigo.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236610f2'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-indigo:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(102,16,242,.25)}.dark-mode .custom-control-input-indigo:focus:not(:checked)~.custom-control-label::before{border-color:#b389f9}.dark-mode .custom-control-input-indigo:not(:disabled):active~.custom-control-label::before{background-color:#d2b9fb;border-color:#d2b9fb}.dark-mode .custom-control-input-purple:checked~.custom-control-label::before{border-color:#6f42c1;background-color:#6f42c1}.dark-mode .custom-control-input-purple.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236f42c1' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-purple.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236f42c1'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-purple:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(111,66,193,.25)}.dark-mode .custom-control-input-purple:focus:not(:checked)~.custom-control-label::before{border-color:#b8a2e0}.dark-mode .custom-control-input-purple:not(:disabled):active~.custom-control-label::before{background-color:#d5c8ed;border-color:#d5c8ed}.dark-mode .custom-control-input-pink:checked~.custom-control-label::before{border-color:#e83e8c;background-color:#e83e8c}.dark-mode .custom-control-input-pink.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23e83e8c' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-pink.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23e83e8c'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-pink:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(232,62,140,.25)}.dark-mode .custom-control-input-pink:focus:not(:checked)~.custom-control-label::before{border-color:#f6b0d0}.dark-mode .custom-control-input-pink:not(:disabled):active~.custom-control-label::before{background-color:#fbddeb;border-color:#fbddeb}.dark-mode .custom-control-input-red:checked~.custom-control-label::before{border-color:#e74c3c;background-color:#e74c3c}.dark-mode .custom-control-input-red.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23e74c3c' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-red.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23e74c3c'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-red:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(231,76,60,.25)}.dark-mode .custom-control-input-red:focus:not(:checked)~.custom-control-label::before{border-color:#f5b4ae}.dark-mode .custom-control-input-red:not(:disabled):active~.custom-control-label::before{background-color:#fbdedb;border-color:#fbdedb}.dark-mode .custom-control-input-orange:checked~.custom-control-label::before{border-color:#fd7e14;background-color:#fd7e14}.dark-mode .custom-control-input-orange.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fd7e14' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-orange.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fd7e14'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-orange:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(253,126,20,.25)}.dark-mode .custom-control-input-orange:focus:not(:checked)~.custom-control-label::before{border-color:#fec392}.dark-mode .custom-control-input-orange:not(:disabled):active~.custom-control-label::before{background-color:#ffdfc5;border-color:#ffdfc5}.dark-mode .custom-control-input-yellow:checked~.custom-control-label::before{border-color:#f39c12;background-color:#f39c12}.dark-mode .custom-control-input-yellow.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f39c12' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-yellow.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23f39c12'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-yellow:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(243,156,18,.25)}.dark-mode .custom-control-input-yellow:focus:not(:checked)~.custom-control-label::before{border-color:#f9cf8b}.dark-mode .custom-control-input-yellow:not(:disabled):active~.custom-control-label::before{background-color:#fce3bc;border-color:#fce3bc}.dark-mode .custom-control-input-green:checked~.custom-control-label::before{border-color:#00bc8c;background-color:#00bc8c}.dark-mode .custom-control-input-green.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2300bc8c' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-green.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2300bc8c'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-green:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(0,188,140,.25)}.dark-mode .custom-control-input-green:focus:not(:checked)~.custom-control-label::before{border-color:#3dffcd}.dark-mode .custom-control-input-green:not(:disabled):active~.custom-control-label::before{background-color:#70ffda;border-color:#70ffda}.dark-mode .custom-control-input-teal:checked~.custom-control-label::before{border-color:#20c997;background-color:#20c997}.dark-mode .custom-control-input-teal.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2320c997' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-teal.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%2320c997'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-teal:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(32,201,151,.25)}.dark-mode .custom-control-input-teal:focus:not(:checked)~.custom-control-label::before{border-color:#7eeaca}.dark-mode .custom-control-input-teal:not(:disabled):active~.custom-control-label::before{background-color:#aaf1dc;border-color:#aaf1dc}.dark-mode .custom-control-input-cyan:checked~.custom-control-label::before{border-color:#3498db;background-color:#3498db}.dark-mode .custom-control-input-cyan.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%233498db' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-cyan.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%233498db'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-cyan:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(52,152,219,.25)}.dark-mode .custom-control-input-cyan:focus:not(:checked)~.custom-control-label::before{border-color:#a0cfee}.dark-mode .custom-control-input-cyan:not(:disabled):active~.custom-control-label::before{background-color:#cce5f6;border-color:#cce5f6}.dark-mode .custom-control-input-white:checked~.custom-control-label::before{border-color:#fff;background-color:#fff}.dark-mode .custom-control-input-white.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-white.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-white:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(255,255,255,.25)}.dark-mode .custom-control-input-white:focus:not(:checked)~.custom-control-label::before{border-color:#fff}.dark-mode .custom-control-input-white:not(:disabled):active~.custom-control-label::before{background-color:#fff;border-color:#fff}.dark-mode .custom-control-input-gray:checked~.custom-control-label::before{border-color:#6c757d;background-color:#6c757d}.dark-mode .custom-control-input-gray.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%236c757d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-gray.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%236c757d'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-gray:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(108,117,125,.25)}.dark-mode .custom-control-input-gray:focus:not(:checked)~.custom-control-label::before{border-color:#afb5ba}.dark-mode .custom-control-input-gray:not(:disabled):active~.custom-control-label::before{background-color:#caced1;border-color:#caced1}.dark-mode .custom-control-input-gray-dark:checked~.custom-control-label::before{border-color:#343a40;background-color:#343a40}.dark-mode .custom-control-input-gray-dark.custom-control-input-outline:checked[type=checkbox]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23343a40' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-gray-dark.custom-control-input-outline:checked[type=radio]~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23343a40'/%3E%3C/svg%3E")!important}.dark-mode .custom-control-input-gray-dark:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,0 0 0 .2rem rgba(52,58,64,.25)}.dark-mode .custom-control-input-gray-dark:focus:not(:checked)~.custom-control-label::before{border-color:#6d7a86}.dark-mode .custom-control-input-gray-dark:not(:disabled):active~.custom-control-label::before{background-color:#88939e;border-color:#88939e}.progress{box-shadow:none;border-radius:1px}.progress.vertical{display:inline-block;height:200px;margin-right:10px;position:relative;width:30px}.progress.vertical>.progress-bar{bottom:0;position:absolute;width:100%}.progress.vertical.progress-sm,.progress.vertical.sm{width:20px}.progress.vertical.progress-xs,.progress.vertical.xs{width:10px}.progress.vertical.progress-xxs,.progress.vertical.xxs{width:3px}.progress-group{margin-bottom:.5rem}.progress-sm{height:10px}.progress-xs{height:7px}.progress-xxs{height:3px}.table tr>td .progress{margin:0}.dark-mode .progress{background:#454d55}.card-primary:not(.card-outline)>.card-header{background-color:#007bff}.card-primary:not(.card-outline)>.card-header,.card-primary:not(.card-outline)>.card-header a{color:#fff}.card-primary:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-primary.card-outline{border-top:3px solid #007bff}.card-primary.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-primary.card-outline-tabs>.card-header a.active{border-top:3px solid #007bff}.bg-gradient-primary>.card-header .btn-tool,.bg-primary>.card-header .btn-tool,.card-primary:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-primary>.card-header .btn-tool:hover,.bg-primary>.card-header .btn-tool:hover,.card-primary:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-primary .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-primary .bootstrap-datetimepicker-widget .table th,.card.bg-primary .bootstrap-datetimepicker-widget .table td,.card.bg-primary .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-primary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#0067d6;color:#fff}.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.today::before,.card.bg-primary .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.active,.card.bg-primary .bootstrap-datetimepicker-widget table td.active:hover{background-color:#3395ff;color:#fff}.card-secondary:not(.card-outline)>.card-header{background-color:#6c757d}.card-secondary:not(.card-outline)>.card-header,.card-secondary:not(.card-outline)>.card-header a{color:#fff}.card-secondary:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-secondary.card-outline{border-top:3px solid #6c757d}.card-secondary.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-secondary.card-outline-tabs>.card-header a.active{border-top:3px solid #6c757d}.bg-gradient-secondary>.card-header .btn-tool,.bg-secondary>.card-header .btn-tool,.card-secondary:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-secondary>.card-header .btn-tool:hover,.bg-secondary>.card-header .btn-tool:hover,.card-secondary:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget .table th,.card.bg-secondary .bootstrap-datetimepicker-widget .table td,.card.bg-secondary .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#596167;color:#fff}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.today::before,.card.bg-secondary .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.active,.card.bg-secondary .bootstrap-datetimepicker-widget table td.active:hover{background-color:#868e96;color:#fff}.card-success:not(.card-outline)>.card-header{background-color:#28a745}.card-success:not(.card-outline)>.card-header,.card-success:not(.card-outline)>.card-header a{color:#fff}.card-success:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-success.card-outline{border-top:3px solid #28a745}.card-success.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-success.card-outline-tabs>.card-header a.active{border-top:3px solid #28a745}.bg-gradient-success>.card-header .btn-tool,.bg-success>.card-header .btn-tool,.card-success:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-success>.card-header .btn-tool:hover,.bg-success>.card-header .btn-tool:hover,.card-success:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-success .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-success .bootstrap-datetimepicker-widget .table th,.card.bg-success .bootstrap-datetimepicker-widget .table td,.card.bg-success .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-success .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#208637;color:#fff}.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.today::before,.card.bg-success .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.active,.card.bg-success .bootstrap-datetimepicker-widget table td.active:hover{background-color:#34ce57;color:#fff}.card-info:not(.card-outline)>.card-header{background-color:#17a2b8}.card-info:not(.card-outline)>.card-header,.card-info:not(.card-outline)>.card-header a{color:#fff}.card-info:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-info.card-outline{border-top:3px solid #17a2b8}.card-info.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-info.card-outline-tabs>.card-header a.active{border-top:3px solid #17a2b8}.bg-gradient-info>.card-header .btn-tool,.bg-info>.card-header .btn-tool,.card-info:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-info>.card-header .btn-tool:hover,.bg-info>.card-header .btn-tool:hover,.card-info:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-info .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-info .bootstrap-datetimepicker-widget .table th,.card.bg-info .bootstrap-datetimepicker-widget .table td,.card.bg-info .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-info .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#128294;color:#fff}.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.today::before,.card.bg-info .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.active,.card.bg-info .bootstrap-datetimepicker-widget table td.active:hover{background-color:#1fc8e3;color:#fff}.card-warning:not(.card-outline)>.card-header{background-color:#ffc107}.card-warning:not(.card-outline)>.card-header,.card-warning:not(.card-outline)>.card-header a{color:#1f2d3d}.card-warning:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-warning.card-outline{border-top:3px solid #ffc107}.card-warning.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-warning.card-outline-tabs>.card-header a.active{border-top:3px solid #ffc107}.bg-gradient-warning>.card-header .btn-tool,.bg-warning>.card-header .btn-tool,.card-warning:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-warning>.card-header .btn-tool:hover,.bg-warning>.card-header .btn-tool:hover,.card-warning:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-warning .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-warning .bootstrap-datetimepicker-widget .table th,.card.bg-warning .bootstrap-datetimepicker-widget .table td,.card.bg-warning .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-warning .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#dda600;color:#1f2d3d}.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.today::before,.card.bg-warning .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.active,.card.bg-warning .bootstrap-datetimepicker-widget table td.active:hover{background-color:#ffce3a;color:#1f2d3d}.card-danger:not(.card-outline)>.card-header{background-color:#dc3545}.card-danger:not(.card-outline)>.card-header,.card-danger:not(.card-outline)>.card-header a{color:#fff}.card-danger:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-danger.card-outline{border-top:3px solid #dc3545}.card-danger.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-danger.card-outline-tabs>.card-header a.active{border-top:3px solid #dc3545}.bg-danger>.card-header .btn-tool,.bg-gradient-danger>.card-header .btn-tool,.card-danger:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-danger>.card-header .btn-tool:hover,.bg-gradient-danger>.card-header .btn-tool:hover,.card-danger:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-danger .bootstrap-datetimepicker-widget .table td,.card.bg-danger .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-danger .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-danger .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-danger .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-danger .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-danger .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-danger .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-danger .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#c62232;color:#fff}.card.bg-danger .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-danger .bootstrap-datetimepicker-widget table td.active,.card.bg-danger .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.active:hover{background-color:#e4606d;color:#fff}.card-light:not(.card-outline)>.card-header{background-color:#f8f9fa}.card-light:not(.card-outline)>.card-header,.card-light:not(.card-outline)>.card-header a{color:#1f2d3d}.card-light:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-light.card-outline{border-top:3px solid #f8f9fa}.card-light.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-light.card-outline-tabs>.card-header a.active{border-top:3px solid #f8f9fa}.bg-gradient-light>.card-header .btn-tool,.bg-light>.card-header .btn-tool,.card-light:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-light>.card-header .btn-tool:hover,.bg-light>.card-header .btn-tool:hover,.card-light:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-light .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-light .bootstrap-datetimepicker-widget .table th,.card.bg-light .bootstrap-datetimepicker-widget .table td,.card.bg-light .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-light .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#e0e5e9;color:#1f2d3d}.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.today::before,.card.bg-light .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.active,.card.bg-light .bootstrap-datetimepicker-widget table td.active:hover{background-color:#fff;color:#1f2d3d}.card-dark:not(.card-outline)>.card-header{background-color:#343a40}.card-dark:not(.card-outline)>.card-header,.card-dark:not(.card-outline)>.card-header a{color:#fff}.card-dark:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-dark.card-outline{border-top:3px solid #343a40}.card-dark.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-dark.card-outline-tabs>.card-header a.active{border-top:3px solid #343a40}.bg-dark>.card-header .btn-tool,.bg-gradient-dark>.card-header .btn-tool,.card-dark:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-dark>.card-header .btn-tool:hover,.bg-gradient-dark>.card-header .btn-tool:hover,.card-dark:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-dark .bootstrap-datetimepicker-widget .table td,.card.bg-dark .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-dark .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-dark .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#222629;color:#fff}.card.bg-dark .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-dark .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.active:hover{background-color:#4b545c;color:#fff}.card-lightblue:not(.card-outline)>.card-header{background-color:#3c8dbc}.card-lightblue:not(.card-outline)>.card-header,.card-lightblue:not(.card-outline)>.card-header a{color:#fff}.card-lightblue:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-lightblue.card-outline{border-top:3px solid #3c8dbc}.card-lightblue.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-lightblue.card-outline-tabs>.card-header a.active{border-top:3px solid #3c8dbc}.bg-gradient-lightblue>.card-header .btn-tool,.bg-lightblue>.card-header .btn-tool,.card-lightblue:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-lightblue>.card-header .btn-tool:hover,.bg-lightblue>.card-header .btn-tool:hover,.card-lightblue:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget .table th,.card.bg-lightblue .bootstrap-datetimepicker-widget .table td,.card.bg-lightblue .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#32769d;color:#fff}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.today::before,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.active,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.active:hover{background-color:#5fa4cc;color:#fff}.card-navy:not(.card-outline)>.card-header{background-color:#001f3f}.card-navy:not(.card-outline)>.card-header,.card-navy:not(.card-outline)>.card-header a{color:#fff}.card-navy:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-navy.card-outline{border-top:3px solid #001f3f}.card-navy.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-navy.card-outline-tabs>.card-header a.active{border-top:3px solid #001f3f}.bg-gradient-navy>.card-header .btn-tool,.bg-navy>.card-header .btn-tool,.card-navy:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-navy>.card-header .btn-tool:hover,.bg-navy>.card-header .btn-tool:hover,.card-navy:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-navy .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-navy .bootstrap-datetimepicker-widget .table th,.card.bg-navy .bootstrap-datetimepicker-widget .table td,.card.bg-navy .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-navy .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#000b16;color:#fff}.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.today::before,.card.bg-navy .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.active,.card.bg-navy .bootstrap-datetimepicker-widget table td.active:hover{background-color:#003872;color:#fff}.card-olive:not(.card-outline)>.card-header{background-color:#3d9970}.card-olive:not(.card-outline)>.card-header,.card-olive:not(.card-outline)>.card-header a{color:#fff}.card-olive:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-olive.card-outline{border-top:3px solid #3d9970}.card-olive.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-olive.card-outline-tabs>.card-header a.active{border-top:3px solid #3d9970}.bg-gradient-olive>.card-header .btn-tool,.bg-olive>.card-header .btn-tool,.card-olive:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-olive>.card-header .btn-tool:hover,.bg-olive>.card-header .btn-tool:hover,.card-olive:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-olive .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-olive .bootstrap-datetimepicker-widget .table th,.card.bg-olive .bootstrap-datetimepicker-widget .table td,.card.bg-olive .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-olive .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#317c5b;color:#fff}.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.today::before,.card.bg-olive .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.active,.card.bg-olive .bootstrap-datetimepicker-widget table td.active:hover{background-color:#50b98a;color:#fff}.card-lime:not(.card-outline)>.card-header{background-color:#01ff70}.card-lime:not(.card-outline)>.card-header,.card-lime:not(.card-outline)>.card-header a{color:#1f2d3d}.card-lime:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-lime.card-outline{border-top:3px solid #01ff70}.card-lime.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-lime.card-outline-tabs>.card-header a.active{border-top:3px solid #01ff70}.bg-gradient-lime>.card-header .btn-tool,.bg-lime>.card-header .btn-tool,.card-lime:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-lime>.card-header .btn-tool:hover,.bg-lime>.card-header .btn-tool:hover,.card-lime:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-lime .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-lime .bootstrap-datetimepicker-widget .table th,.card.bg-lime .bootstrap-datetimepicker-widget .table td,.card.bg-lime .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-lime .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#00d75e;color:#1f2d3d}.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.today::before,.card.bg-lime .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.active,.card.bg-lime .bootstrap-datetimepicker-widget table td.active:hover{background-color:#34ff8d;color:#1f2d3d}.card-fuchsia:not(.card-outline)>.card-header{background-color:#f012be}.card-fuchsia:not(.card-outline)>.card-header,.card-fuchsia:not(.card-outline)>.card-header a{color:#fff}.card-fuchsia:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-fuchsia.card-outline{border-top:3px solid #f012be}.card-fuchsia.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-fuchsia.card-outline-tabs>.card-header a.active{border-top:3px solid #f012be}.bg-fuchsia>.card-header .btn-tool,.bg-gradient-fuchsia>.card-header .btn-tool,.card-fuchsia:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-fuchsia>.card-header .btn-tool:hover,.bg-gradient-fuchsia>.card-header .btn-tool:hover,.card-fuchsia:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-fuchsia .bootstrap-datetimepicker-widget .table td,.card.bg-fuchsia .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#cc0da1;color:#fff}.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.active,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.active:hover{background-color:#f342cb;color:#fff}.card-maroon:not(.card-outline)>.card-header{background-color:#d81b60}.card-maroon:not(.card-outline)>.card-header,.card-maroon:not(.card-outline)>.card-header a{color:#fff}.card-maroon:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-maroon.card-outline{border-top:3px solid #d81b60}.card-maroon.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-maroon.card-outline-tabs>.card-header a.active{border-top:3px solid #d81b60}.bg-gradient-maroon>.card-header .btn-tool,.bg-maroon>.card-header .btn-tool,.card-maroon:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-maroon>.card-header .btn-tool:hover,.bg-maroon>.card-header .btn-tool:hover,.card-maroon:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget .table th,.card.bg-maroon .bootstrap-datetimepicker-widget .table td,.card.bg-maroon .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#b41650;color:#fff}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.today::before,.card.bg-maroon .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.active,.card.bg-maroon .bootstrap-datetimepicker-widget table td.active:hover{background-color:#e73f7c;color:#fff}.card-blue:not(.card-outline)>.card-header{background-color:#007bff}.card-blue:not(.card-outline)>.card-header,.card-blue:not(.card-outline)>.card-header a{color:#fff}.card-blue:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-blue.card-outline{border-top:3px solid #007bff}.card-blue.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-blue.card-outline-tabs>.card-header a.active{border-top:3px solid #007bff}.bg-blue>.card-header .btn-tool,.bg-gradient-blue>.card-header .btn-tool,.card-blue:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-blue>.card-header .btn-tool:hover,.bg-gradient-blue>.card-header .btn-tool:hover,.card-blue:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-blue .bootstrap-datetimepicker-widget .table td,.card.bg-blue .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-blue .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-blue .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-blue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-blue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-blue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-blue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-blue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#0067d6;color:#fff}.card.bg-blue .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-blue .bootstrap-datetimepicker-widget table td.active,.card.bg-blue .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.active:hover{background-color:#3395ff;color:#fff}.card-indigo:not(.card-outline)>.card-header{background-color:#6610f2}.card-indigo:not(.card-outline)>.card-header,.card-indigo:not(.card-outline)>.card-header a{color:#fff}.card-indigo:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-indigo.card-outline{border-top:3px solid #6610f2}.card-indigo.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-indigo.card-outline-tabs>.card-header a.active{border-top:3px solid #6610f2}.bg-gradient-indigo>.card-header .btn-tool,.bg-indigo>.card-header .btn-tool,.card-indigo:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-indigo>.card-header .btn-tool:hover,.bg-indigo>.card-header .btn-tool:hover,.card-indigo:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget .table th,.card.bg-indigo .bootstrap-datetimepicker-widget .table td,.card.bg-indigo .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#550bce;color:#fff}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.today::before,.card.bg-indigo .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.active,.card.bg-indigo .bootstrap-datetimepicker-widget table td.active:hover{background-color:#8540f5;color:#fff}.card-purple:not(.card-outline)>.card-header{background-color:#6f42c1}.card-purple:not(.card-outline)>.card-header,.card-purple:not(.card-outline)>.card-header a{color:#fff}.card-purple:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-purple.card-outline{border-top:3px solid #6f42c1}.card-purple.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-purple.card-outline-tabs>.card-header a.active{border-top:3px solid #6f42c1}.bg-gradient-purple>.card-header .btn-tool,.bg-purple>.card-header .btn-tool,.card-purple:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-purple>.card-header .btn-tool:hover,.bg-purple>.card-header .btn-tool:hover,.card-purple:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-purple .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-purple .bootstrap-datetimepicker-widget .table th,.card.bg-purple .bootstrap-datetimepicker-widget .table td,.card.bg-purple .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-purple .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#5d36a4;color:#fff}.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.today::before,.card.bg-purple .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.active,.card.bg-purple .bootstrap-datetimepicker-widget table td.active:hover{background-color:#8c68ce;color:#fff}.card-pink:not(.card-outline)>.card-header{background-color:#e83e8c}.card-pink:not(.card-outline)>.card-header,.card-pink:not(.card-outline)>.card-header a{color:#fff}.card-pink:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-pink.card-outline{border-top:3px solid #e83e8c}.card-pink.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-pink.card-outline-tabs>.card-header a.active{border-top:3px solid #e83e8c}.bg-gradient-pink>.card-header .btn-tool,.bg-pink>.card-header .btn-tool,.card-pink:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-pink>.card-header .btn-tool:hover,.bg-pink>.card-header .btn-tool:hover,.card-pink:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-pink .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-pink .bootstrap-datetimepicker-widget .table th,.card.bg-pink .bootstrap-datetimepicker-widget .table td,.card.bg-pink .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-pink .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#e21b76;color:#fff}.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.today::before,.card.bg-pink .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.active,.card.bg-pink .bootstrap-datetimepicker-widget table td.active:hover{background-color:#ed6ca7;color:#fff}.card-red:not(.card-outline)>.card-header{background-color:#dc3545}.card-red:not(.card-outline)>.card-header,.card-red:not(.card-outline)>.card-header a{color:#fff}.card-red:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-red.card-outline{border-top:3px solid #dc3545}.card-red.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-red.card-outline-tabs>.card-header a.active{border-top:3px solid #dc3545}.bg-gradient-red>.card-header .btn-tool,.bg-red>.card-header .btn-tool,.card-red:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-red>.card-header .btn-tool:hover,.bg-red>.card-header .btn-tool:hover,.card-red:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-red .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-red .bootstrap-datetimepicker-widget .table th,.card.bg-red .bootstrap-datetimepicker-widget .table td,.card.bg-red .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-red .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#c62232;color:#fff}.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.today::before,.card.bg-red .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.active,.card.bg-red .bootstrap-datetimepicker-widget table td.active:hover{background-color:#e4606d;color:#fff}.card-orange:not(.card-outline)>.card-header{background-color:#fd7e14}.card-orange:not(.card-outline)>.card-header,.card-orange:not(.card-outline)>.card-header a{color:#1f2d3d}.card-orange:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-orange.card-outline{border-top:3px solid #fd7e14}.card-orange.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-orange.card-outline-tabs>.card-header a.active{border-top:3px solid #fd7e14}.bg-gradient-orange>.card-header .btn-tool,.bg-orange>.card-header .btn-tool,.card-orange:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-orange>.card-header .btn-tool:hover,.bg-orange>.card-header .btn-tool:hover,.card-orange:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-orange .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-orange .bootstrap-datetimepicker-widget .table th,.card.bg-orange .bootstrap-datetimepicker-widget .table td,.card.bg-orange .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-orange .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#e66a02;color:#1f2d3d}.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.today::before,.card.bg-orange .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.active,.card.bg-orange .bootstrap-datetimepicker-widget table td.active:hover{background-color:#fd9a47;color:#1f2d3d}.card-yellow:not(.card-outline)>.card-header{background-color:#ffc107}.card-yellow:not(.card-outline)>.card-header,.card-yellow:not(.card-outline)>.card-header a{color:#1f2d3d}.card-yellow:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-yellow.card-outline{border-top:3px solid #ffc107}.card-yellow.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-yellow.card-outline-tabs>.card-header a.active{border-top:3px solid #ffc107}.bg-gradient-yellow>.card-header .btn-tool,.bg-yellow>.card-header .btn-tool,.card-yellow:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-yellow>.card-header .btn-tool:hover,.bg-yellow>.card-header .btn-tool:hover,.card-yellow:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget .table th,.card.bg-yellow .bootstrap-datetimepicker-widget .table td,.card.bg-yellow .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#dda600;color:#1f2d3d}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.today::before,.card.bg-yellow .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.active,.card.bg-yellow .bootstrap-datetimepicker-widget table td.active:hover{background-color:#ffce3a;color:#1f2d3d}.card-green:not(.card-outline)>.card-header{background-color:#28a745}.card-green:not(.card-outline)>.card-header,.card-green:not(.card-outline)>.card-header a{color:#fff}.card-green:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-green.card-outline{border-top:3px solid #28a745}.card-green.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-green.card-outline-tabs>.card-header a.active{border-top:3px solid #28a745}.bg-gradient-green>.card-header .btn-tool,.bg-green>.card-header .btn-tool,.card-green:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-green>.card-header .btn-tool:hover,.bg-green>.card-header .btn-tool:hover,.card-green:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-green .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-green .bootstrap-datetimepicker-widget .table th,.card.bg-green .bootstrap-datetimepicker-widget .table td,.card.bg-green .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-green .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#208637;color:#fff}.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.today::before,.card.bg-green .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.active,.card.bg-green .bootstrap-datetimepicker-widget table td.active:hover{background-color:#34ce57;color:#fff}.card-teal:not(.card-outline)>.card-header{background-color:#20c997}.card-teal:not(.card-outline)>.card-header,.card-teal:not(.card-outline)>.card-header a{color:#fff}.card-teal:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-teal.card-outline{border-top:3px solid #20c997}.card-teal.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-teal.card-outline-tabs>.card-header a.active{border-top:3px solid #20c997}.bg-gradient-teal>.card-header .btn-tool,.bg-teal>.card-header .btn-tool,.card-teal:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-teal>.card-header .btn-tool:hover,.bg-teal>.card-header .btn-tool:hover,.card-teal:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-teal .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-teal .bootstrap-datetimepicker-widget .table th,.card.bg-teal .bootstrap-datetimepicker-widget .table td,.card.bg-teal .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-teal .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#1aa67d;color:#fff}.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.today::before,.card.bg-teal .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.active,.card.bg-teal .bootstrap-datetimepicker-widget table td.active:hover{background-color:#3ce0af;color:#fff}.card-cyan:not(.card-outline)>.card-header{background-color:#17a2b8}.card-cyan:not(.card-outline)>.card-header,.card-cyan:not(.card-outline)>.card-header a{color:#fff}.card-cyan:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-cyan.card-outline{border-top:3px solid #17a2b8}.card-cyan.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-cyan.card-outline-tabs>.card-header a.active{border-top:3px solid #17a2b8}.bg-cyan>.card-header .btn-tool,.bg-gradient-cyan>.card-header .btn-tool,.card-cyan:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-cyan>.card-header .btn-tool:hover,.bg-gradient-cyan>.card-header .btn-tool:hover,.card-cyan:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-cyan .bootstrap-datetimepicker-widget .table td,.card.bg-cyan .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-cyan .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#128294;color:#fff}.card.bg-cyan .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-cyan .bootstrap-datetimepicker-widget table td.active,.card.bg-cyan .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.active:hover{background-color:#1fc8e3;color:#fff}.card-white:not(.card-outline)>.card-header{background-color:#fff}.card-white:not(.card-outline)>.card-header,.card-white:not(.card-outline)>.card-header a{color:#1f2d3d}.card-white:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-white.card-outline{border-top:3px solid #fff}.card-white.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-white.card-outline-tabs>.card-header a.active{border-top:3px solid #fff}.bg-gradient-white>.card-header .btn-tool,.bg-white>.card-header .btn-tool,.card-white:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-white>.card-header .btn-tool:hover,.bg-white>.card-header .btn-tool:hover,.card-white:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-white .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-white .bootstrap-datetimepicker-widget .table th,.card.bg-white .bootstrap-datetimepicker-widget .table td,.card.bg-white .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-white .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#ebebeb;color:#1f2d3d}.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.today::before,.card.bg-white .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.active,.card.bg-white .bootstrap-datetimepicker-widget table td.active:hover{background-color:#fff;color:#1f2d3d}.card-gray:not(.card-outline)>.card-header{background-color:#6c757d}.card-gray:not(.card-outline)>.card-header,.card-gray:not(.card-outline)>.card-header a{color:#fff}.card-gray:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-gray.card-outline{border-top:3px solid #6c757d}.card-gray.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-gray.card-outline-tabs>.card-header a.active{border-top:3px solid #6c757d}.bg-gradient-gray>.card-header .btn-tool,.bg-gray>.card-header .btn-tool,.card-gray:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-gray>.card-header .btn-tool:hover,.bg-gray>.card-header .btn-tool:hover,.card-gray:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-gray .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-gray .bootstrap-datetimepicker-widget .table th,.card.bg-gray .bootstrap-datetimepicker-widget .table td,.card.bg-gray .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gray .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#596167;color:#fff}.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gray .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.active,.card.bg-gray .bootstrap-datetimepicker-widget table td.active:hover{background-color:#868e96;color:#fff}.card-gray-dark:not(.card-outline)>.card-header{background-color:#343a40}.card-gray-dark:not(.card-outline)>.card-header,.card-gray-dark:not(.card-outline)>.card-header a{color:#fff}.card-gray-dark:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-gray-dark.card-outline{border-top:3px solid #343a40}.card-gray-dark.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-gray-dark.card-outline-tabs>.card-header a.active{border-top:3px solid #343a40}.bg-gradient-gray-dark>.card-header .btn-tool,.bg-gray-dark>.card-header .btn-tool,.card-gray-dark:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-gray-dark>.card-header .btn-tool:hover,.bg-gray-dark>.card-header .btn-tool:hover,.card-gray-dark:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget .table th,.card.bg-gray-dark .bootstrap-datetimepicker-widget .table td,.card.bg-gray-dark .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#222629;color:#fff}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.active:hover{background-color:#4b545c;color:#fff}.card{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);margin-bottom:1rem}.card.bg-dark .card-header{border-color:#383f45}.card.bg-dark,.card.bg-dark .card-body{color:#fff}.card.maximized-card{height:100%!important;left:0;max-height:100%!important;max-width:100%!important;position:fixed;top:0;width:100%!important;z-index:1040}.card.maximized-card.was-collapsed .card-body{display:block!important}.card.maximized-card .card-body{overflow:auto}.card.maximized-card [data-card-widgett=collapse]{display:none}.card.maximized-card .card-footer,.card.maximized-card .card-header{border-radius:0!important}.card.collapsed-card .card-body,.card.collapsed-card .card-footer{display:none}.card .nav.flex-column>li{border-bottom:1px solid rgba(0,0,0,.125);margin:0}.card .nav.flex-column>li:last-of-type{border-bottom:0}.card.height-control .card-body{max-height:300px;overflow:auto}.card .border-right{border-right:1px solid rgba(0,0,0,.125)}.card .border-left{border-left:1px solid rgba(0,0,0,.125)}.card.card-tabs:not(.card-outline)>.card-header{border-bottom:0}.card.card-tabs:not(.card-outline)>.card-header .nav-item:first-child .nav-link{border-left-color:transparent}.card.card-tabs.card-outline .nav-item{border-bottom:0}.card.card-tabs.card-outline .nav-item:first-child .nav-link{border-left:0;margin-left:0}.card.card-tabs .card-tools{margin:.3rem .5rem}.card.card-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}.card.card-outline-tabs{border-top:0}.card.card-outline-tabs .card-header .nav-item:first-child .nav-link{border-left:0;margin-left:0}.card.card-outline-tabs .card-header a{border-top:3px solid transparent}.card.card-outline-tabs .card-header a:hover{border-top:3px solid #dee2e6}.card.card-outline-tabs .card-header a.active:hover{margin-top:0}.card.card-outline-tabs .card-tools{margin:.5rem .5rem .3rem}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-outline-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}html.maximized-card{overflow:hidden}.card-body::after,.card-footer::after,.card-header::after{display:block;clear:both;content:""}.card-header{background-color:transparent;border-bottom:1px solid rgba(0,0,0,.125);padding:.75rem 1.25rem;position:relative;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.collapsed-card .card-header{border-bottom:0}.card-header>.card-tools{float:right;margin-right:-.625rem}.card-header>.card-tools .input-group,.card-header>.card-tools .nav,.card-header>.card-tools .pagination{margin-bottom:-.3rem;margin-top:-.3rem}.card-header>.card-tools [data-toggle=tooltip]{position:relative}.card-title{float:left;font-size:1.1rem;font-weight:400;margin:0}.card-text{clear:both}.btn-tool{background-color:transparent;color:#adb5bd;font-size:.875rem;margin:-.75rem 0;padding:.25rem .5rem}.btn-group.show .btn-tool,.btn-tool:hover{color:#495057}.btn-tool:focus,.show .btn-tool{box-shadow:none!important}.text-sm .card-title{font-size:1rem}.text-sm .nav-link{padding:.4rem .8rem}.card-body>.table{margin-bottom:0}.card-body>.table>thead>tr>td,.card-body>.table>thead>tr>th{border-top-width:0}.card-body .fc{margin-top:5px}.card-body .full-width-chart{margin:-19px}.card-body.p-0 .full-width-chart{margin:-9px}.chart-legend{padding-left:0;list-style:none;margin:10px 0}@media (max-width:576px){.chart-legend>li{float:left;margin-right:10px}}.card-comments{background-color:#f8f9fa}.card-comments .card-comment{border-bottom:1px solid #e9ecef;padding:8px 0}.card-comments .card-comment::after{display:block;clear:both;content:""}.card-comments .card-comment:last-of-type{border-bottom:0}.card-comments .card-comment:first-of-type{padding-top:0}.card-comments .card-comment img{height:1.875rem;width:1.875rem;float:left}.card-comments .comment-text{color:#78838e;margin-left:40px}.card-comments .username{color:#495057;display:block;font-weight:600}.card-comments .text-muted{font-size:12px;font-weight:400}.todo-list{list-style:none;margin:0;overflow:auto;padding:0}.todo-list>li{border-radius:2px;background-color:#f8f9fa;border-left:2px solid #e9ecef;color:#495057;margin-bottom:2px;padding:10px}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type=checkbox]{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;font-weight:600;margin-left:5px}.todo-list>li .badge{font-size:.7rem;margin-left:10px}.todo-list>li .tools{color:#dc3545;display:none;float:right}.todo-list>li .tools>.fa,.todo-list>li .tools>.fab,.todo-list>li .tools>.fad,.todo-list>li .tools>.fal,.todo-list>li .tools>.far,.todo-list>li .tools>.fas,.todo-list>li .tools>.ion,.todo-list>li .tools>.svg-inline--fa{cursor:pointer;margin-right:5px}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#697582}.todo-list>li.done .text{font-weight:500;text-decoration:line-through}.todo-list>li.done .badge{background-color:#adb5bd!important}.todo-list .primary{border-left-color:#007bff}.todo-list .secondary{border-left-color:#6c757d}.todo-list .success{border-left-color:#28a745}.todo-list .info{border-left-color:#17a2b8}.todo-list .warning{border-left-color:#ffc107}.todo-list .danger{border-left-color:#dc3545}.todo-list .light{border-left-color:#f8f9fa}.todo-list .dark{border-left-color:#343a40}.todo-list .lightblue{border-left-color:#3c8dbc}.todo-list .navy{border-left-color:#001f3f}.todo-list .olive{border-left-color:#3d9970}.todo-list .lime{border-left-color:#01ff70}.todo-list .fuchsia{border-left-color:#f012be}.todo-list .maroon{border-left-color:#d81b60}.todo-list .blue{border-left-color:#007bff}.todo-list .indigo{border-left-color:#6610f2}.todo-list .purple{border-left-color:#6f42c1}.todo-list .pink{border-left-color:#e83e8c}.todo-list .red{border-left-color:#dc3545}.todo-list .orange{border-left-color:#fd7e14}.todo-list .yellow{border-left-color:#ffc107}.todo-list .green{border-left-color:#28a745}.todo-list .teal{border-left-color:#20c997}.todo-list .cyan{border-left-color:#17a2b8}.todo-list .white{border-left-color:#fff}.todo-list .gray{border-left-color:#6c757d}.todo-list .gray-dark{border-left-color:#343a40}.todo-list .handle{cursor:move;display:inline-block;margin:0 5px}.card-input{max-width:200px}.card-default .nav-item:first-child .nav-link{border-left:0}.dark-mode .card-primary:not(.card-outline)>.card-header{background-color:#3f6791}.dark-mode .card-primary:not(.card-outline)>.card-header,.dark-mode .card-primary:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-primary:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-primary.card-outline{border-top:3px solid #3f6791}.dark-mode .card-primary.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-primary.card-outline-tabs>.card-header a.active{border-top:3px solid #3f6791}.dark-mode .bg-gradient-primary>.card-header .btn-tool,.dark-mode .bg-primary>.card-header .btn-tool,.dark-mode .card-primary:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-primary>.card-header .btn-tool:hover,.dark-mode .bg-primary>.card-header .btn-tool:hover,.dark-mode .card-primary:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#335375;color:#fff}.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-primary .bootstrap-datetimepicker-widget table td.active:hover{background-color:#5080b3;color:#fff}.dark-mode .card-secondary:not(.card-outline)>.card-header{background-color:#6c757d}.dark-mode .card-secondary:not(.card-outline)>.card-header,.dark-mode .card-secondary:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-secondary:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-secondary.card-outline{border-top:3px solid #6c757d}.dark-mode .card-secondary.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-secondary.card-outline-tabs>.card-header a.active{border-top:3px solid #6c757d}.dark-mode .bg-gradient-secondary>.card-header .btn-tool,.dark-mode .bg-secondary>.card-header .btn-tool,.dark-mode .card-secondary:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-secondary>.card-header .btn-tool:hover,.dark-mode .bg-secondary>.card-header .btn-tool:hover,.dark-mode .card-secondary:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#596167;color:#fff}.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-secondary .bootstrap-datetimepicker-widget table td.active:hover{background-color:#868e96;color:#fff}.dark-mode .card-success:not(.card-outline)>.card-header{background-color:#00bc8c}.dark-mode .card-success:not(.card-outline)>.card-header,.dark-mode .card-success:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-success:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-success.card-outline{border-top:3px solid #00bc8c}.dark-mode .card-success.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-success.card-outline-tabs>.card-header a.active{border-top:3px solid #00bc8c}.dark-mode .bg-gradient-success>.card-header .btn-tool,.dark-mode .bg-success>.card-header .btn-tool,.dark-mode .card-success:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-success>.card-header .btn-tool:hover,.dark-mode .bg-success>.card-header .btn-tool:hover,.dark-mode .card-success:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#00936e;color:#fff}.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-success .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-success .bootstrap-datetimepicker-widget table td.active:hover{background-color:#00efb2;color:#fff}.dark-mode .card-info:not(.card-outline)>.card-header{background-color:#3498db}.dark-mode .card-info:not(.card-outline)>.card-header,.dark-mode .card-info:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-info:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-info.card-outline{border-top:3px solid #3498db}.dark-mode .card-info.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-info.card-outline-tabs>.card-header a.active{border-top:3px solid #3498db}.dark-mode .bg-gradient-info>.card-header .btn-tool,.dark-mode .bg-info>.card-header .btn-tool,.dark-mode .card-info:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-info>.card-header .btn-tool:hover,.dark-mode .bg-info>.card-header .btn-tool:hover,.dark-mode .card-info:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#2383c4;color:#fff}.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-info .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-info .bootstrap-datetimepicker-widget table td.active:hover{background-color:#5faee3;color:#fff}.dark-mode .card-warning:not(.card-outline)>.card-header{background-color:#f39c12}.dark-mode .card-warning:not(.card-outline)>.card-header,.dark-mode .card-warning:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-warning:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-warning.card-outline{border-top:3px solid #f39c12}.dark-mode .card-warning.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-warning.card-outline-tabs>.card-header a.active{border-top:3px solid #f39c12}.dark-mode .bg-gradient-warning>.card-header .btn-tool,.dark-mode .bg-warning>.card-header .btn-tool,.dark-mode .card-warning:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-warning>.card-header .btn-tool:hover,.dark-mode .bg-warning>.card-header .btn-tool:hover,.dark-mode .card-warning:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#d2850b;color:#1f2d3d}.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-warning .bootstrap-datetimepicker-widget table td.active:hover{background-color:#f5b043;color:#1f2d3d}.dark-mode .card-danger:not(.card-outline)>.card-header{background-color:#e74c3c}.dark-mode .card-danger:not(.card-outline)>.card-header,.dark-mode .card-danger:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-danger:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-danger.card-outline{border-top:3px solid #e74c3c}.dark-mode .card-danger.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-danger.card-outline-tabs>.card-header a.active{border-top:3px solid #e74c3c}.dark-mode .bg-danger>.card-header .btn-tool,.dark-mode .bg-gradient-danger>.card-header .btn-tool,.dark-mode .card-danger:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-danger>.card-header .btn-tool:hover,.dark-mode .bg-gradient-danger>.card-header .btn-tool:hover,.dark-mode .card-danger:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#df2e1b;color:#fff}.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-danger .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.active:hover{background-color:#ed7669;color:#fff}.dark-mode .card-light:not(.card-outline)>.card-header{background-color:#f8f9fa}.dark-mode .card-light:not(.card-outline)>.card-header,.dark-mode .card-light:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-light:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-light.card-outline{border-top:3px solid #f8f9fa}.dark-mode .card-light.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-light.card-outline-tabs>.card-header a.active{border-top:3px solid #f8f9fa}.dark-mode .bg-gradient-light>.card-header .btn-tool,.dark-mode .bg-light>.card-header .btn-tool,.dark-mode .card-light:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-light>.card-header .btn-tool:hover,.dark-mode .bg-light>.card-header .btn-tool:hover,.dark-mode .card-light:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#e0e5e9;color:#1f2d3d}.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-light .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-light .bootstrap-datetimepicker-widget table td.active:hover{background-color:#fff;color:#1f2d3d}.dark-mode .card-dark:not(.card-outline)>.card-header{background-color:#343a40}.dark-mode .card-dark:not(.card-outline)>.card-header,.dark-mode .card-dark:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-dark:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-dark.card-outline{border-top:3px solid #343a40}.dark-mode .card-dark.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-dark.card-outline-tabs>.card-header a.active{border-top:3px solid #343a40}.dark-mode .bg-dark>.card-header .btn-tool,.dark-mode .bg-gradient-dark>.card-header .btn-tool,.dark-mode .card-dark:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-dark>.card-header .btn-tool:hover,.dark-mode .bg-gradient-dark>.card-header .btn-tool:hover,.dark-mode .card-dark:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#222629;color:#fff}.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-dark .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.active:hover{background-color:#4b545c;color:#fff}.dark-mode .card-lightblue:not(.card-outline)>.card-header{background-color:#86bad8}.dark-mode .card-lightblue:not(.card-outline)>.card-header,.dark-mode .card-lightblue:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-lightblue:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-lightblue.card-outline{border-top:3px solid #86bad8}.dark-mode .card-lightblue.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-lightblue.card-outline-tabs>.card-header a.active{border-top:3px solid #86bad8}.dark-mode .bg-gradient-lightblue>.card-header .btn-tool,.dark-mode .bg-lightblue>.card-header .btn-tool,.dark-mode .card-lightblue:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-lightblue>.card-header .btn-tool:hover,.dark-mode .bg-lightblue>.card-header .btn-tool:hover,.dark-mode .card-lightblue:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#67a8ce;color:#1f2d3d}.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-lightblue .bootstrap-datetimepicker-widget table td.active:hover{background-color:#acd0e5;color:#1f2d3d}.dark-mode .card-navy:not(.card-outline)>.card-header{background-color:#002c59}.dark-mode .card-navy:not(.card-outline)>.card-header,.dark-mode .card-navy:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-navy:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-navy.card-outline{border-top:3px solid #002c59}.dark-mode .card-navy.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-navy.card-outline-tabs>.card-header a.active{border-top:3px solid #002c59}.dark-mode .bg-gradient-navy>.card-header .btn-tool,.dark-mode .bg-navy>.card-header .btn-tool,.dark-mode .card-navy:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-navy>.card-header .btn-tool:hover,.dark-mode .bg-navy>.card-header .btn-tool:hover,.dark-mode .card-navy:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#001730;color:#fff}.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-navy .bootstrap-datetimepicker-widget table td.active:hover{background-color:#00458c;color:#fff}.dark-mode .card-olive:not(.card-outline)>.card-header{background-color:#74c8a3}.dark-mode .card-olive:not(.card-outline)>.card-header,.dark-mode .card-olive:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-olive:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-olive.card-outline{border-top:3px solid #74c8a3}.dark-mode .card-olive.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-olive.card-outline-tabs>.card-header a.active{border-top:3px solid #74c8a3}.dark-mode .bg-gradient-olive>.card-header .btn-tool,.dark-mode .bg-olive>.card-header .btn-tool,.dark-mode .card-olive:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-olive>.card-header .btn-tool:hover,.dark-mode .bg-olive>.card-header .btn-tool:hover,.dark-mode .card-olive:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#57bc8f;color:#1f2d3d}.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-olive .bootstrap-datetimepicker-widget table td.active:hover{background-color:#99d6bb;color:#1f2d3d}.dark-mode .card-lime:not(.card-outline)>.card-header{background-color:#67ffa9}.dark-mode .card-lime:not(.card-outline)>.card-header,.dark-mode .card-lime:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-lime:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-lime.card-outline{border-top:3px solid #67ffa9}.dark-mode .card-lime.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-lime.card-outline-tabs>.card-header a.active{border-top:3px solid #67ffa9}.dark-mode .bg-gradient-lime>.card-header .btn-tool,.dark-mode .bg-lime>.card-header .btn-tool,.dark-mode .card-lime:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-lime>.card-header .btn-tool:hover,.dark-mode .bg-lime>.card-header .btn-tool:hover,.dark-mode .card-lime:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#3eff92;color:#1f2d3d}.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-lime .bootstrap-datetimepicker-widget table td.active:hover{background-color:#9affc6;color:#1f2d3d}.dark-mode .card-fuchsia:not(.card-outline)>.card-header{background-color:#f672d8}.dark-mode .card-fuchsia:not(.card-outline)>.card-header,.dark-mode .card-fuchsia:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-fuchsia:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-fuchsia.card-outline{border-top:3px solid #f672d8}.dark-mode .card-fuchsia.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-fuchsia.card-outline-tabs>.card-header a.active{border-top:3px solid #f672d8}.dark-mode .bg-fuchsia>.card-header .btn-tool,.dark-mode .bg-gradient-fuchsia>.card-header .btn-tool,.dark-mode .card-fuchsia:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-fuchsia>.card-header .btn-tool:hover,.dark-mode .bg-gradient-fuchsia>.card-header .btn-tool:hover,.dark-mode .card-fuchsia:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#f44cce;color:#1f2d3d}.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-fuchsia .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.active:hover{background-color:#f9a2e5;color:#1f2d3d}.dark-mode .card-maroon:not(.card-outline)>.card-header{background-color:#ed6c9b}.dark-mode .card-maroon:not(.card-outline)>.card-header,.dark-mode .card-maroon:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-maroon:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-maroon.card-outline{border-top:3px solid #ed6c9b}.dark-mode .card-maroon.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-maroon.card-outline-tabs>.card-header a.active{border-top:3px solid #ed6c9b}.dark-mode .bg-gradient-maroon>.card-header .btn-tool,.dark-mode .bg-maroon>.card-header .btn-tool,.dark-mode .card-maroon:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-maroon>.card-header .btn-tool:hover,.dark-mode .bg-maroon>.card-header .btn-tool:hover,.dark-mode .card-maroon:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#e84883;color:#1f2d3d}.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-maroon .bootstrap-datetimepicker-widget table td.active:hover{background-color:#f29aba;color:#1f2d3d}.dark-mode .card-blue:not(.card-outline)>.card-header{background-color:#3f6791}.dark-mode .card-blue:not(.card-outline)>.card-header,.dark-mode .card-blue:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-blue:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-blue.card-outline{border-top:3px solid #3f6791}.dark-mode .card-blue.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-blue.card-outline-tabs>.card-header a.active{border-top:3px solid #3f6791}.dark-mode .bg-blue>.card-header .btn-tool,.dark-mode .bg-gradient-blue>.card-header .btn-tool,.dark-mode .card-blue:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-blue>.card-header .btn-tool:hover,.dark-mode .bg-gradient-blue>.card-header .btn-tool:hover,.dark-mode .card-blue:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#335375;color:#fff}.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-blue .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.active:hover{background-color:#5080b3;color:#fff}.dark-mode .card-indigo:not(.card-outline)>.card-header{background-color:#6610f2}.dark-mode .card-indigo:not(.card-outline)>.card-header,.dark-mode .card-indigo:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-indigo:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-indigo.card-outline{border-top:3px solid #6610f2}.dark-mode .card-indigo.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-indigo.card-outline-tabs>.card-header a.active{border-top:3px solid #6610f2}.dark-mode .bg-gradient-indigo>.card-header .btn-tool,.dark-mode .bg-indigo>.card-header .btn-tool,.dark-mode .card-indigo:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-indigo>.card-header .btn-tool:hover,.dark-mode .bg-indigo>.card-header .btn-tool:hover,.dark-mode .card-indigo:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#550bce;color:#fff}.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-indigo .bootstrap-datetimepicker-widget table td.active:hover{background-color:#8540f5;color:#fff}.dark-mode .card-purple:not(.card-outline)>.card-header{background-color:#6f42c1}.dark-mode .card-purple:not(.card-outline)>.card-header,.dark-mode .card-purple:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-purple:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-purple.card-outline{border-top:3px solid #6f42c1}.dark-mode .card-purple.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-purple.card-outline-tabs>.card-header a.active{border-top:3px solid #6f42c1}.dark-mode .bg-gradient-purple>.card-header .btn-tool,.dark-mode .bg-purple>.card-header .btn-tool,.dark-mode .card-purple:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-purple>.card-header .btn-tool:hover,.dark-mode .bg-purple>.card-header .btn-tool:hover,.dark-mode .card-purple:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#5d36a4;color:#fff}.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-purple .bootstrap-datetimepicker-widget table td.active:hover{background-color:#8c68ce;color:#fff}.dark-mode .card-pink:not(.card-outline)>.card-header{background-color:#e83e8c}.dark-mode .card-pink:not(.card-outline)>.card-header,.dark-mode .card-pink:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-pink:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-pink.card-outline{border-top:3px solid #e83e8c}.dark-mode .card-pink.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-pink.card-outline-tabs>.card-header a.active{border-top:3px solid #e83e8c}.dark-mode .bg-gradient-pink>.card-header .btn-tool,.dark-mode .bg-pink>.card-header .btn-tool,.dark-mode .card-pink:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-pink>.card-header .btn-tool:hover,.dark-mode .bg-pink>.card-header .btn-tool:hover,.dark-mode .card-pink:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#e21b76;color:#fff}.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-pink .bootstrap-datetimepicker-widget table td.active:hover{background-color:#ed6ca7;color:#fff}.dark-mode .card-red:not(.card-outline)>.card-header{background-color:#e74c3c}.dark-mode .card-red:not(.card-outline)>.card-header,.dark-mode .card-red:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-red:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-red.card-outline{border-top:3px solid #e74c3c}.dark-mode .card-red.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-red.card-outline-tabs>.card-header a.active{border-top:3px solid #e74c3c}.dark-mode .bg-gradient-red>.card-header .btn-tool,.dark-mode .bg-red>.card-header .btn-tool,.dark-mode .card-red:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-red>.card-header .btn-tool:hover,.dark-mode .bg-red>.card-header .btn-tool:hover,.dark-mode .card-red:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#df2e1b;color:#fff}.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-red .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-red .bootstrap-datetimepicker-widget table td.active:hover{background-color:#ed7669;color:#fff}.dark-mode .card-orange:not(.card-outline)>.card-header{background-color:#fd7e14}.dark-mode .card-orange:not(.card-outline)>.card-header,.dark-mode .card-orange:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-orange:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-orange.card-outline{border-top:3px solid #fd7e14}.dark-mode .card-orange.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-orange.card-outline-tabs>.card-header a.active{border-top:3px solid #fd7e14}.dark-mode .bg-gradient-orange>.card-header .btn-tool,.dark-mode .bg-orange>.card-header .btn-tool,.dark-mode .card-orange:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-orange>.card-header .btn-tool:hover,.dark-mode .bg-orange>.card-header .btn-tool:hover,.dark-mode .card-orange:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#e66a02;color:#1f2d3d}.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-orange .bootstrap-datetimepicker-widget table td.active:hover{background-color:#fd9a47;color:#1f2d3d}.dark-mode .card-yellow:not(.card-outline)>.card-header{background-color:#f39c12}.dark-mode .card-yellow:not(.card-outline)>.card-header,.dark-mode .card-yellow:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-yellow:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-yellow.card-outline{border-top:3px solid #f39c12}.dark-mode .card-yellow.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-yellow.card-outline-tabs>.card-header a.active{border-top:3px solid #f39c12}.dark-mode .bg-gradient-yellow>.card-header .btn-tool,.dark-mode .bg-yellow>.card-header .btn-tool,.dark-mode .card-yellow:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-yellow>.card-header .btn-tool:hover,.dark-mode .bg-yellow>.card-header .btn-tool:hover,.dark-mode .card-yellow:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#d2850b;color:#1f2d3d}.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-yellow .bootstrap-datetimepicker-widget table td.active:hover{background-color:#f5b043;color:#1f2d3d}.dark-mode .card-green:not(.card-outline)>.card-header{background-color:#00bc8c}.dark-mode .card-green:not(.card-outline)>.card-header,.dark-mode .card-green:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-green:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-green.card-outline{border-top:3px solid #00bc8c}.dark-mode .card-green.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-green.card-outline-tabs>.card-header a.active{border-top:3px solid #00bc8c}.dark-mode .bg-gradient-green>.card-header .btn-tool,.dark-mode .bg-green>.card-header .btn-tool,.dark-mode .card-green:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-green>.card-header .btn-tool:hover,.dark-mode .bg-green>.card-header .btn-tool:hover,.dark-mode .card-green:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#00936e;color:#fff}.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-green .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-green .bootstrap-datetimepicker-widget table td.active:hover{background-color:#00efb2;color:#fff}.dark-mode .card-teal:not(.card-outline)>.card-header{background-color:#20c997}.dark-mode .card-teal:not(.card-outline)>.card-header,.dark-mode .card-teal:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-teal:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-teal.card-outline{border-top:3px solid #20c997}.dark-mode .card-teal.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-teal.card-outline-tabs>.card-header a.active{border-top:3px solid #20c997}.dark-mode .bg-gradient-teal>.card-header .btn-tool,.dark-mode .bg-teal>.card-header .btn-tool,.dark-mode .card-teal:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-teal>.card-header .btn-tool:hover,.dark-mode .bg-teal>.card-header .btn-tool:hover,.dark-mode .card-teal:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#1aa67d;color:#fff}.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-teal .bootstrap-datetimepicker-widget table td.active:hover{background-color:#3ce0af;color:#fff}.dark-mode .card-cyan:not(.card-outline)>.card-header{background-color:#3498db}.dark-mode .card-cyan:not(.card-outline)>.card-header,.dark-mode .card-cyan:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-cyan:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-cyan.card-outline{border-top:3px solid #3498db}.dark-mode .card-cyan.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-cyan.card-outline-tabs>.card-header a.active{border-top:3px solid #3498db}.dark-mode .bg-cyan>.card-header .btn-tool,.dark-mode .bg-gradient-cyan>.card-header .btn-tool,.dark-mode .card-cyan:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-cyan>.card-header .btn-tool:hover,.dark-mode .bg-gradient-cyan>.card-header .btn-tool:hover,.dark-mode .card-cyan:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#2383c4;color:#fff}.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-cyan .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.active:hover{background-color:#5faee3;color:#fff}.dark-mode .card-white:not(.card-outline)>.card-header{background-color:#fff}.dark-mode .card-white:not(.card-outline)>.card-header,.dark-mode .card-white:not(.card-outline)>.card-header a{color:#1f2d3d}.dark-mode .card-white:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-white.card-outline{border-top:3px solid #fff}.dark-mode .card-white.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-white.card-outline-tabs>.card-header a.active{border-top:3px solid #fff}.dark-mode .bg-gradient-white>.card-header .btn-tool,.dark-mode .bg-white>.card-header .btn-tool,.dark-mode .card-white:not(.card-outline)>.card-header .btn-tool{color:rgba(31,45,61,.8)}.dark-mode .bg-gradient-white>.card-header .btn-tool:hover,.dark-mode .bg-white>.card-header .btn-tool:hover,.dark-mode .card-white:not(.card-outline)>.card-header .btn-tool:hover{color:#1f2d3d}.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#ebebeb;color:#1f2d3d}.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-white .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-white .bootstrap-datetimepicker-widget table td.active:hover{background-color:#fff;color:#1f2d3d}.dark-mode .card-gray:not(.card-outline)>.card-header{background-color:#6c757d}.dark-mode .card-gray:not(.card-outline)>.card-header,.dark-mode .card-gray:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-gray:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-gray.card-outline{border-top:3px solid #6c757d}.dark-mode .card-gray.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-gray.card-outline-tabs>.card-header a.active{border-top:3px solid #6c757d}.dark-mode .bg-gradient-gray>.card-header .btn-tool,.dark-mode .bg-gray>.card-header .btn-tool,.dark-mode .card-gray:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-gray>.card-header .btn-tool:hover,.dark-mode .bg-gray>.card-header .btn-tool:hover,.dark-mode .card-gray:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#596167;color:#fff}.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gray .bootstrap-datetimepicker-widget table td.active:hover{background-color:#868e96;color:#fff}.dark-mode .card-gray-dark:not(.card-outline)>.card-header{background-color:#343a40}.dark-mode .card-gray-dark:not(.card-outline)>.card-header,.dark-mode .card-gray-dark:not(.card-outline)>.card-header a{color:#fff}.dark-mode .card-gray-dark:not(.card-outline)>.card-header a.active{color:#1f2d3d}.dark-mode .card-gray-dark.card-outline{border-top:3px solid #343a40}.dark-mode .card-gray-dark.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.dark-mode .card-gray-dark.card-outline-tabs>.card-header a.active{border-top:3px solid #343a40}.dark-mode .bg-gradient-gray-dark>.card-header .btn-tool,.dark-mode .bg-gray-dark>.card-header .btn-tool,.dark-mode .card-gray-dark:not(.card-outline)>.card-header .btn-tool{color:rgba(255,255,255,.8)}.dark-mode .bg-gradient-gray-dark>.card-header .btn-tool:hover,.dark-mode .bg-gray-dark>.card-header .btn-tool:hover,.dark-mode .card-gray-dark:not(.card-outline)>.card-header .btn-tool:hover{color:#fff}.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget .table th,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget .table td,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget .table th{border:none}.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table td.day:hover,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table td.hour:hover,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table td.minute:hover,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table td.second:hover,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background-color:#222629;color:#fff}.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.today::before,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.active:hover,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table td.active,.dark-mode .card.bg-gray-dark .bootstrap-datetimepicker-widget table td.active:hover{background-color:#4b545c;color:#fff}.dark-mode .card{background-color:#343a40;color:#fff}.dark-mode .card .card{background-color:#3f474e;color:#fff}.dark-mode .card .nav.flex-column>li{border-bottom-color:#6c757d}.dark-mode .card .card-footer{background-color:rgba(0,0,0,.1)}.dark-mode .card.card-outline-tabs .card-header a:hover{border-color:#6c757d;border-bottom-color:transparent}.dark-mode .card:not(.card-outline)>.card-header a.active{color:#fff}.dark-mode .card-comments{background-color:#373d44}.dark-mode .card-comments .username{color:#ced4da}.dark-mode .card-comments .card-comment{border-bottom-color:#454d55}.dark-mode .todo-list>li{background-color:#3f474e;border-color:#454d55;color:#fff}.dark-mode .todo-list .primary{border-left-color:#3f6791}.dark-mode .todo-list .secondary{border-left-color:#6c757d}.dark-mode .todo-list .success{border-left-color:#00bc8c}.dark-mode .todo-list .info{border-left-color:#3498db}.dark-mode .todo-list .warning{border-left-color:#f39c12}.dark-mode .todo-list .danger{border-left-color:#e74c3c}.dark-mode .todo-list .light{border-left-color:#f8f9fa}.dark-mode .todo-list .dark{border-left-color:#343a40}.dark-mode .todo-list .lightblue{border-left-color:#86bad8}.dark-mode .todo-list .navy{border-left-color:#002c59}.dark-mode .todo-list .olive{border-left-color:#74c8a3}.dark-mode .todo-list .lime{border-left-color:#67ffa9}.dark-mode .todo-list .fuchsia{border-left-color:#f672d8}.dark-mode .todo-list .maroon{border-left-color:#ed6c9b}.dark-mode .todo-list .blue{border-left-color:#3f6791}.dark-mode .todo-list .indigo{border-left-color:#6610f2}.dark-mode .todo-list .purple{border-left-color:#6f42c1}.dark-mode .todo-list .pink{border-left-color:#e83e8c}.dark-mode .todo-list .red{border-left-color:#e74c3c}.dark-mode .todo-list .orange{border-left-color:#fd7e14}.dark-mode .todo-list .yellow{border-left-color:#f39c12}.dark-mode .todo-list .green{border-left-color:#00bc8c}.dark-mode .todo-list .teal{border-left-color:#20c997}.dark-mode .todo-list .cyan{border-left-color:#3498db}.dark-mode .todo-list .white{border-left-color:#fff}.dark-mode .todo-list .gray{border-left-color:#6c757d}.dark-mode .todo-list .gray-dark{border-left-color:#343a40}.modal-dialog .overlay{display:-webkit-flex;display:-ms-flexbox;display:flex;position:absolute;left:0;top:0;bottom:0;right:0;margin:-1px;z-index:1052;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;background-color:rgba(0,0,0,.7);color:#666f76;border-radius:.3rem}.modal-content.bg-warning .modal-footer,.modal-content.bg-warning .modal-header{border-color:#343a40}.modal-content.bg-danger .close,.modal-content.bg-danger .mailbox-attachment-close,.modal-content.bg-info .close,.modal-content.bg-info .mailbox-attachment-close,.modal-content.bg-primary .close,.modal-content.bg-primary .mailbox-attachment-close,.modal-content.bg-secondary .close,.modal-content.bg-secondary .mailbox-attachment-close,.modal-content.bg-success .close,.modal-content.bg-success .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .modal-footer,.dark-mode .modal-header{border-color:#6c757d}.dark-mode .modal-content{background-color:#343a40}.dark-mode .modal-content.bg-warning .modal-footer,.dark-mode .modal-content.bg-warning .modal-header{border-color:#6c757d}.dark-mode .modal-content.bg-warning .close,.dark-mode .modal-content.bg-warning .mailbox-attachment-close{color:#343a40!important;text-shadow:0 1px 0 #495057!important}.dark-mode .modal-content.bg-danger .modal-footer,.dark-mode .modal-content.bg-danger .modal-header,.dark-mode .modal-content.bg-info .modal-footer,.dark-mode .modal-content.bg-info .modal-header,.dark-mode .modal-content.bg-primary .modal-footer,.dark-mode .modal-content.bg-primary .modal-header,.dark-mode .modal-content.bg-secondary .modal-footer,.dark-mode .modal-content.bg-secondary .modal-header,.dark-mode .modal-content.bg-success .modal-footer,.dark-mode .modal-content.bg-success .modal-header{border-color:#fff}.toasts-top-right{position:absolute;right:0;top:0;z-index:1040}.toasts-top-right.fixed{position:fixed}.toasts-top-left{left:0;position:absolute;top:0;z-index:1040}.toasts-top-left.fixed{position:fixed}.toasts-bottom-right{bottom:0;position:absolute;right:0;z-index:1040}.toasts-bottom-right.fixed{position:fixed}.toasts-bottom-left{bottom:0;left:0;position:absolute;z-index:1040}.toasts-bottom-left.fixed{position:fixed}.dark-mode .toast{background-color:rgba(52,58,64,.85);color:#fff}.dark-mode .toast .toast-header{background-color:rgba(52,58,64,.7);color:#f8f9fa}.dark-mode .toast.bg-primary{background-color:rgba(63,103,145,.9)!important}.dark-mode .toast.bg-primary .close,.dark-mode .toast.bg-primary .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-primary .toast-header{background-color:rgba(63,103,145,.85);color:#fff}.dark-mode .toast.bg-secondary{background-color:rgba(108,117,125,.9)!important}.dark-mode .toast.bg-secondary .close,.dark-mode .toast.bg-secondary .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-secondary .toast-header{background-color:rgba(108,117,125,.85);color:#fff}.dark-mode .toast.bg-success{background-color:rgba(0,188,140,.9)!important}.dark-mode .toast.bg-success .close,.dark-mode .toast.bg-success .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-success .toast-header{background-color:rgba(0,188,140,.85);color:#fff}.dark-mode .toast.bg-info{background-color:rgba(52,152,219,.9)!important}.dark-mode .toast.bg-info .close,.dark-mode .toast.bg-info .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-info .toast-header{background-color:rgba(52,152,219,.85);color:#fff}.dark-mode .toast.bg-warning{background-color:rgba(243,156,18,.9)!important}.dark-mode .toast.bg-warning .toast-header{background-color:rgba(243,156,18,.85);color:#1f2d3d}.dark-mode .toast.bg-danger{background-color:rgba(231,76,60,.9)!important}.dark-mode .toast.bg-danger .close,.dark-mode .toast.bg-danger .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-danger .toast-header{background-color:rgba(231,76,60,.85);color:#fff}.dark-mode .toast.bg-light{background-color:rgba(248,249,250,.9)!important}.dark-mode .toast.bg-light .toast-header{background-color:rgba(248,249,250,.85);color:#1f2d3d}.dark-mode .toast.bg-dark{background-color:rgba(52,58,64,.9)!important}.dark-mode .toast.bg-dark .close,.dark-mode .toast.bg-dark .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-dark .toast-header{background-color:rgba(52,58,64,.85);color:#fff}.dark-mode .toast.bg-lightblue{background-color:rgba(134,186,216,.9)!important}.dark-mode .toast.bg-lightblue .toast-header{background-color:rgba(134,186,216,.85);color:#1f2d3d}.dark-mode .toast.bg-navy{background-color:rgba(0,44,89,.9)!important}.dark-mode .toast.bg-navy .close,.dark-mode .toast.bg-navy .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-navy .toast-header{background-color:rgba(0,44,89,.85);color:#fff}.dark-mode .toast.bg-olive{background-color:rgba(116,200,163,.9)!important}.dark-mode .toast.bg-olive .toast-header{background-color:rgba(116,200,163,.85);color:#1f2d3d}.dark-mode .toast.bg-lime{background-color:rgba(103,255,169,.9)!important}.dark-mode .toast.bg-lime .toast-header{background-color:rgba(103,255,169,.85);color:#1f2d3d}.dark-mode .toast.bg-fuchsia{background-color:rgba(246,114,216,.9)!important}.dark-mode .toast.bg-fuchsia .toast-header{background-color:rgba(246,114,216,.85);color:#1f2d3d}.dark-mode .toast.bg-maroon{background-color:rgba(237,108,155,.9)!important}.dark-mode .toast.bg-maroon .toast-header{background-color:rgba(237,108,155,.85);color:#1f2d3d}.dark-mode .toast.bg-blue{background-color:rgba(63,103,145,.9)!important}.dark-mode .toast.bg-blue .close,.dark-mode .toast.bg-blue .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-blue .toast-header{background-color:rgba(63,103,145,.85);color:#fff}.dark-mode .toast.bg-indigo{background-color:rgba(102,16,242,.9)!important}.dark-mode .toast.bg-indigo .close,.dark-mode .toast.bg-indigo .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-indigo .toast-header{background-color:rgba(102,16,242,.85);color:#fff}.dark-mode .toast.bg-purple{background-color:rgba(111,66,193,.9)!important}.dark-mode .toast.bg-purple .close,.dark-mode .toast.bg-purple .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-purple .toast-header{background-color:rgba(111,66,193,.85);color:#fff}.dark-mode .toast.bg-pink{background-color:rgba(232,62,140,.9)!important}.dark-mode .toast.bg-pink .close,.dark-mode .toast.bg-pink .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-pink .toast-header{background-color:rgba(232,62,140,.85);color:#fff}.dark-mode .toast.bg-red{background-color:rgba(231,76,60,.9)!important}.dark-mode .toast.bg-red .close,.dark-mode .toast.bg-red .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-red .toast-header{background-color:rgba(231,76,60,.85);color:#fff}.dark-mode .toast.bg-orange{background-color:rgba(253,126,20,.9)!important}.dark-mode .toast.bg-orange .toast-header{background-color:rgba(253,126,20,.85);color:#1f2d3d}.dark-mode .toast.bg-yellow{background-color:rgba(243,156,18,.9)!important}.dark-mode .toast.bg-yellow .toast-header{background-color:rgba(243,156,18,.85);color:#1f2d3d}.dark-mode .toast.bg-green{background-color:rgba(0,188,140,.9)!important}.dark-mode .toast.bg-green .close,.dark-mode .toast.bg-green .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-green .toast-header{background-color:rgba(0,188,140,.85);color:#fff}.dark-mode .toast.bg-teal{background-color:rgba(32,201,151,.9)!important}.dark-mode .toast.bg-teal .close,.dark-mode .toast.bg-teal .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-teal .toast-header{background-color:rgba(32,201,151,.85);color:#fff}.dark-mode .toast.bg-cyan{background-color:rgba(52,152,219,.9)!important}.dark-mode .toast.bg-cyan .close,.dark-mode .toast.bg-cyan .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-cyan .toast-header{background-color:rgba(52,152,219,.85);color:#fff}.dark-mode .toast.bg-white{background-color:rgba(255,255,255,.9)!important}.dark-mode .toast.bg-white .toast-header{background-color:rgba(255,255,255,.85);color:#1f2d3d}.dark-mode .toast.bg-gray{background-color:rgba(108,117,125,.9)!important}.dark-mode .toast.bg-gray .close,.dark-mode .toast.bg-gray .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-gray .toast-header{background-color:rgba(108,117,125,.85);color:#fff}.dark-mode .toast.bg-gray-dark{background-color:rgba(52,58,64,.9)!important}.dark-mode .toast.bg-gray-dark .close,.dark-mode .toast.bg-gray-dark .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.dark-mode .toast.bg-gray-dark .toast-header{background-color:rgba(52,58,64,.85);color:#fff}.toast.bg-primary{background-color:rgba(0,123,255,.9)!important}.toast.bg-primary .close,.toast.bg-primary .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-primary .toast-header{background-color:rgba(0,123,255,.85);color:#fff}.toast.bg-secondary{background-color:rgba(108,117,125,.9)!important}.toast.bg-secondary .close,.toast.bg-secondary .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-secondary .toast-header{background-color:rgba(108,117,125,.85);color:#fff}.toast.bg-success{background-color:rgba(40,167,69,.9)!important}.toast.bg-success .close,.toast.bg-success .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-success .toast-header{background-color:rgba(40,167,69,.85);color:#fff}.toast.bg-info{background-color:rgba(23,162,184,.9)!important}.toast.bg-info .close,.toast.bg-info .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-info .toast-header{background-color:rgba(23,162,184,.85);color:#fff}.toast.bg-warning{background-color:rgba(255,193,7,.9)!important}.toast.bg-warning .toast-header{background-color:rgba(255,193,7,.85);color:#1f2d3d}.toast.bg-danger{background-color:rgba(220,53,69,.9)!important}.toast.bg-danger .close,.toast.bg-danger .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-danger .toast-header{background-color:rgba(220,53,69,.85);color:#fff}.toast.bg-light{background-color:rgba(248,249,250,.9)!important}.toast.bg-light .toast-header{background-color:rgba(248,249,250,.85);color:#1f2d3d}.toast.bg-dark{background-color:rgba(52,58,64,.9)!important}.toast.bg-dark .close,.toast.bg-dark .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-dark .toast-header{background-color:rgba(52,58,64,.85);color:#fff}.toast.bg-lightblue{background-color:rgba(60,141,188,.9)!important}.toast.bg-lightblue .close,.toast.bg-lightblue .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-lightblue .toast-header{background-color:rgba(60,141,188,.85);color:#fff}.toast.bg-navy{background-color:rgba(0,31,63,.9)!important}.toast.bg-navy .close,.toast.bg-navy .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-navy .toast-header{background-color:rgba(0,31,63,.85);color:#fff}.toast.bg-olive{background-color:rgba(61,153,112,.9)!important}.toast.bg-olive .close,.toast.bg-olive .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-olive .toast-header{background-color:rgba(61,153,112,.85);color:#fff}.toast.bg-lime{background-color:rgba(1,255,112,.9)!important}.toast.bg-lime .toast-header{background-color:rgba(1,255,112,.85);color:#1f2d3d}.toast.bg-fuchsia{background-color:rgba(240,18,190,.9)!important}.toast.bg-fuchsia .close,.toast.bg-fuchsia .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-fuchsia .toast-header{background-color:rgba(240,18,190,.85);color:#fff}.toast.bg-maroon{background-color:rgba(216,27,96,.9)!important}.toast.bg-maroon .close,.toast.bg-maroon .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-maroon .toast-header{background-color:rgba(216,27,96,.85);color:#fff}.toast.bg-blue{background-color:rgba(0,123,255,.9)!important}.toast.bg-blue .close,.toast.bg-blue .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-blue .toast-header{background-color:rgba(0,123,255,.85);color:#fff}.toast.bg-indigo{background-color:rgba(102,16,242,.9)!important}.toast.bg-indigo .close,.toast.bg-indigo .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-indigo .toast-header{background-color:rgba(102,16,242,.85);color:#fff}.toast.bg-purple{background-color:rgba(111,66,193,.9)!important}.toast.bg-purple .close,.toast.bg-purple .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-purple .toast-header{background-color:rgba(111,66,193,.85);color:#fff}.toast.bg-pink{background-color:rgba(232,62,140,.9)!important}.toast.bg-pink .close,.toast.bg-pink .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-pink .toast-header{background-color:rgba(232,62,140,.85);color:#fff}.toast.bg-red{background-color:rgba(220,53,69,.9)!important}.toast.bg-red .close,.toast.bg-red .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-red .toast-header{background-color:rgba(220,53,69,.85);color:#fff}.toast.bg-orange{background-color:rgba(253,126,20,.9)!important}.toast.bg-orange .toast-header{background-color:rgba(253,126,20,.85);color:#1f2d3d}.toast.bg-yellow{background-color:rgba(255,193,7,.9)!important}.toast.bg-yellow .toast-header{background-color:rgba(255,193,7,.85);color:#1f2d3d}.toast.bg-green{background-color:rgba(40,167,69,.9)!important}.toast.bg-green .close,.toast.bg-green .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-green .toast-header{background-color:rgba(40,167,69,.85);color:#fff}.toast.bg-teal{background-color:rgba(32,201,151,.9)!important}.toast.bg-teal .close,.toast.bg-teal .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-teal .toast-header{background-color:rgba(32,201,151,.85);color:#fff}.toast.bg-cyan{background-color:rgba(23,162,184,.9)!important}.toast.bg-cyan .close,.toast.bg-cyan .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-cyan .toast-header{background-color:rgba(23,162,184,.85);color:#fff}.toast.bg-white{background-color:rgba(255,255,255,.9)!important}.toast.bg-white .toast-header{background-color:rgba(255,255,255,.85);color:#1f2d3d}.toast.bg-gray{background-color:rgba(108,117,125,.9)!important}.toast.bg-gray .close,.toast.bg-gray .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-gray .toast-header{background-color:rgba(108,117,125,.85);color:#fff}.toast.bg-gray-dark{background-color:rgba(52,58,64,.9)!important}.toast.bg-gray-dark .close,.toast.bg-gray-dark .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-gray-dark .toast-header{background-color:rgba(52,58,64,.85);color:#fff}.btn.disabled,.btn:disabled{cursor:not-allowed}.btn.btn-flat{border-radius:0;border-width:1px;box-shadow:none}.btn.btn-file{overflow:hidden;position:relative}.btn.btn-file>input[type=file]{background-color:#fff;cursor:inherit;display:block;font-size:100px;min-height:100%;min-width:100%;opacity:0;outline:0;position:absolute;right:0;text-align:right;top:0}.text-sm .btn{font-size:.875rem!important}.btn-default{background-color:#f8f9fa;border-color:#ddd;color:#444}.btn-default.hover,.btn-default:active,.btn-default:hover{background-color:#e9ecef;color:#2b2b2b}.btn-app{border-radius:3px;background-color:#f8f9fa;border:1px solid #ddd;color:#6c757d;font-size:12px;height:60px;margin:0 0 10px 10px;min-width:80px;padding:15px 5px;position:relative;text-align:center}.btn-app>.fa,.btn-app>.fab,.btn-app>.fad,.btn-app>.fal,.btn-app>.far,.btn-app>.fas,.btn-app>.ion,.btn-app>.svg-inline--fa{display:block;font-size:20px}.btn-app>.svg-inline--fa{margin:0 auto}.btn-app:hover{background-color:#f8f9fa;border-color:#aaa;color:#444}.btn-app:active,.btn-app:focus{box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-app>.badge{font-size:10px;font-weight:400;position:absolute;right:-10px;top:-3px}.btn-xs{padding:.125rem .25rem;font-size:.75rem;line-height:1.5;border-radius:.15rem}.dark-mode .btn-app,.dark-mode .btn-default{background-color:#3a4047;color:#fff;border-color:#6c757d}.dark-mode .btn-app:focus,.dark-mode .btn-app:hover,.dark-mode .btn-default:focus,.dark-mode .btn-default:hover{background-color:#3f474e;color:#dee2e6;border-color:#727b84}.dark-mode .btn-light{background-color:#454d55;color:#fff;border-color:#6c757d}.dark-mode .btn-light:focus,.dark-mode .btn-light:hover{background-color:#4b545c;color:#dee2e6;border-color:#78828a}.dark-mode .btn-primary{color:#fff;background-color:#3f6791;border-color:#3f6791;box-shadow:none}.dark-mode .btn-primary:hover{color:#fff;background-color:#335476;border-color:#304e6d}.dark-mode .btn-primary.focus,.dark-mode .btn-primary:focus{color:#fff;background-color:#335476;border-color:#304e6d;box-shadow:0 0 0 0 rgba(92,126,162,.5)}.dark-mode .btn-primary.disabled,.dark-mode .btn-primary:disabled{color:#fff;background-color:#3f6791;border-color:#3f6791}.dark-mode .btn-primary:not(:disabled):not(.disabled).active,.dark-mode .btn-primary:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-primary.dropdown-toggle{color:#fff;background-color:#304e6d;border-color:#2c4765}.dark-mode .btn-primary:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-primary:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(92,126,162,.5)}.dark-mode .btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d;box-shadow:none}.dark-mode .btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.dark-mode .btn-secondary.focus,.dark-mode .btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 0 rgba(130,138,145,.5)}.dark-mode .btn-secondary.disabled,.dark-mode .btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.dark-mode .btn-secondary:not(:disabled):not(.disabled).active,.dark-mode .btn-secondary:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.dark-mode .btn-secondary:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(130,138,145,.5)}.dark-mode .btn-success{color:#fff;background-color:#00bc8c;border-color:#00bc8c;box-shadow:none}.dark-mode .btn-success:hover{color:#fff;background-color:#009670;border-color:#008966}.dark-mode .btn-success.focus,.dark-mode .btn-success:focus{color:#fff;background-color:#009670;border-color:#008966;box-shadow:0 0 0 0 rgba(38,198,157,.5)}.dark-mode .btn-success.disabled,.dark-mode .btn-success:disabled{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.dark-mode .btn-success:not(:disabled):not(.disabled).active,.dark-mode .btn-success:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-success.dropdown-toggle{color:#fff;background-color:#008966;border-color:#007c5d}.dark-mode .btn-success:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-success:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(38,198,157,.5)}.dark-mode .btn-info{color:#fff;background-color:#3498db;border-color:#3498db;box-shadow:none}.dark-mode .btn-info:hover{color:#fff;background-color:#2384c6;border-color:#217dbb}.dark-mode .btn-info.focus,.dark-mode .btn-info:focus{color:#fff;background-color:#2384c6;border-color:#217dbb;box-shadow:0 0 0 0 rgba(82,167,224,.5)}.dark-mode .btn-info.disabled,.dark-mode .btn-info:disabled{color:#fff;background-color:#3498db;border-color:#3498db}.dark-mode .btn-info:not(:disabled):not(.disabled).active,.dark-mode .btn-info:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-info.dropdown-toggle{color:#fff;background-color:#217dbb;border-color:#1f76b0}.dark-mode .btn-info:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-info:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(82,167,224,.5)}.dark-mode .btn-warning{color:#1f2d3d;background-color:#f39c12;border-color:#f39c12;box-shadow:none}.dark-mode .btn-warning:hover{color:#fff;background-color:#d4860b;border-color:#c87f0a}.dark-mode .btn-warning.focus,.dark-mode .btn-warning:focus{color:#fff;background-color:#d4860b;border-color:#c87f0a;box-shadow:0 0 0 0 rgba(211,139,24,.5)}.dark-mode .btn-warning.disabled,.dark-mode .btn-warning:disabled{color:#1f2d3d;background-color:#f39c12;border-color:#f39c12}.dark-mode .btn-warning:not(:disabled):not(.disabled).active,.dark-mode .btn-warning:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-warning.dropdown-toggle{color:#fff;background-color:#c87f0a;border-color:#bc770a}.dark-mode .btn-warning:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-warning:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(211,139,24,.5)}.dark-mode .btn-danger{color:#fff;background-color:#e74c3c;border-color:#e74c3c;box-shadow:none}.dark-mode .btn-danger:hover{color:#fff;background-color:#e12e1c;border-color:#d62c1a}.dark-mode .btn-danger.focus,.dark-mode .btn-danger:focus{color:#fff;background-color:#e12e1c;border-color:#d62c1a;box-shadow:0 0 0 0 rgba(235,103,89,.5)}.dark-mode .btn-danger.disabled,.dark-mode .btn-danger:disabled{color:#fff;background-color:#e74c3c;border-color:#e74c3c}.dark-mode .btn-danger:not(:disabled):not(.disabled).active,.dark-mode .btn-danger:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-danger.dropdown-toggle{color:#fff;background-color:#d62c1a;border-color:#ca2a19}.dark-mode .btn-danger:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-danger:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(235,103,89,.5)}.dark-mode .btn-light{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa;box-shadow:none}.dark-mode .btn-light:hover{color:#1f2d3d;background-color:#e2e6ea;border-color:#dae0e5}.dark-mode .btn-light.focus,.dark-mode .btn-light:focus{color:#1f2d3d;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 0 rgba(215,218,222,.5)}.dark-mode .btn-light.disabled,.dark-mode .btn-light:disabled{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.dark-mode .btn-light:not(:disabled):not(.disabled).active,.dark-mode .btn-light:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-light.dropdown-toggle{color:#1f2d3d;background-color:#dae0e5;border-color:#d3d9df}.dark-mode .btn-light:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-light:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-light.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(215,218,222,.5)}.dark-mode .btn-dark{color:#fff;background-color:#343a40;border-color:#343a40;box-shadow:none}.dark-mode .btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.dark-mode .btn-dark.focus,.dark-mode .btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 0 rgba(82,88,93,.5)}.dark-mode .btn-dark.disabled,.dark-mode .btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.dark-mode .btn-dark:not(:disabled):not(.disabled).active,.dark-mode .btn-dark:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.dark-mode .btn-dark:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-dark:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(82,88,93,.5)}.dark-mode .btn-outline-primary{color:#3f6791;border-color:#3f6791}.dark-mode .btn-outline-primary:hover{color:#fff;background-color:#3f6791;border-color:#3f6791}.dark-mode .btn-outline-primary.focus,.dark-mode .btn-outline-primary:focus{box-shadow:0 0 0 0 rgba(63,103,145,.5)}.dark-mode .btn-outline-primary.disabled,.dark-mode .btn-outline-primary:disabled{color:#3f6791;background-color:transparent}.dark-mode .btn-outline-primary:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-primary:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-primary.dropdown-toggle{color:#fff;background-color:#3f6791;border-color:#3f6791}.dark-mode .btn-outline-primary:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(63,103,145,.5)}.dark-mode .btn-outline-secondary{color:#6c757d;border-color:#6c757d}.dark-mode .btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.dark-mode .btn-outline-secondary.focus,.dark-mode .btn-outline-secondary:focus{box-shadow:0 0 0 0 rgba(108,117,125,.5)}.dark-mode .btn-outline-secondary.disabled,.dark-mode .btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.dark-mode .btn-outline-secondary:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.dark-mode .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(108,117,125,.5)}.dark-mode .btn-outline-success{color:#00bc8c;border-color:#00bc8c}.dark-mode .btn-outline-success:hover{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.dark-mode .btn-outline-success.focus,.dark-mode .btn-outline-success:focus{box-shadow:0 0 0 0 rgba(0,188,140,.5)}.dark-mode .btn-outline-success.disabled,.dark-mode .btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.dark-mode .btn-outline-success:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-success:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-success.dropdown-toggle{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.dark-mode .btn-outline-success:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(0,188,140,.5)}.dark-mode .btn-outline-info{color:#3498db;border-color:#3498db}.dark-mode .btn-outline-info:hover{color:#fff;background-color:#3498db;border-color:#3498db}.dark-mode .btn-outline-info.focus,.dark-mode .btn-outline-info:focus{box-shadow:0 0 0 0 rgba(52,152,219,.5)}.dark-mode .btn-outline-info.disabled,.dark-mode .btn-outline-info:disabled{color:#3498db;background-color:transparent}.dark-mode .btn-outline-info:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-info:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-info.dropdown-toggle{color:#fff;background-color:#3498db;border-color:#3498db}.dark-mode .btn-outline-info:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(52,152,219,.5)}.dark-mode .btn-outline-warning{color:#f39c12;border-color:#f39c12}.dark-mode .btn-outline-warning:hover{color:#1f2d3d;background-color:#f39c12;border-color:#f39c12}.dark-mode .btn-outline-warning.focus,.dark-mode .btn-outline-warning:focus{box-shadow:0 0 0 0 rgba(243,156,18,.5)}.dark-mode .btn-outline-warning.disabled,.dark-mode .btn-outline-warning:disabled{color:#f39c12;background-color:transparent}.dark-mode .btn-outline-warning:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-warning:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-warning.dropdown-toggle{color:#1f2d3d;background-color:#f39c12;border-color:#f39c12}.dark-mode .btn-outline-warning:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(243,156,18,.5)}.dark-mode .btn-outline-danger{color:#e74c3c;border-color:#e74c3c}.dark-mode .btn-outline-danger:hover{color:#fff;background-color:#e74c3c;border-color:#e74c3c}.dark-mode .btn-outline-danger.focus,.dark-mode .btn-outline-danger:focus{box-shadow:0 0 0 0 rgba(231,76,60,.5)}.dark-mode .btn-outline-danger.disabled,.dark-mode .btn-outline-danger:disabled{color:#e74c3c;background-color:transparent}.dark-mode .btn-outline-danger:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-danger:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-danger.dropdown-toggle{color:#fff;background-color:#e74c3c;border-color:#e74c3c}.dark-mode .btn-outline-danger:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(231,76,60,.5)}.dark-mode .btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.dark-mode .btn-outline-light:hover{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.dark-mode .btn-outline-light.focus,.dark-mode .btn-outline-light:focus{box-shadow:0 0 0 0 rgba(248,249,250,.5)}.dark-mode .btn-outline-light.disabled,.dark-mode .btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.dark-mode .btn-outline-light:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-light:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-light.dropdown-toggle{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.dark-mode .btn-outline-light:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(248,249,250,.5)}.dark-mode .btn-outline-dark{color:#343a40;border-color:#343a40}.dark-mode .btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.dark-mode .btn-outline-dark.focus,.dark-mode .btn-outline-dark:focus{box-shadow:0 0 0 0 rgba(52,58,64,.5)}.dark-mode .btn-outline-dark.disabled,.dark-mode .btn-outline-dark:disabled{color:#343a40;background-color:transparent}.dark-mode .btn-outline-dark:not(:disabled):not(.disabled).active,.dark-mode .btn-outline-dark:not(:disabled):not(.disabled):active,.show>.dark-mode .btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.dark-mode .btn-outline-dark:not(:disabled):not(.disabled).active:focus,.dark-mode .btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.dark-mode .btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(52,58,64,.5)}.callout{border-radius:.25rem;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24);background-color:#fff;border-left:5px solid #e9ecef;margin-bottom:1rem;padding:1rem}.callout a{color:#495057;text-decoration:underline}.callout a:hover{color:#e9ecef}.callout p:last-child{margin-bottom:0}.callout.callout-danger{border-left-color:#bd2130}.callout.callout-warning{border-left-color:#d39e00}.callout.callout-info{border-left-color:#117a8b}.callout.callout-success{border-left-color:#1e7e34}.dark-mode .callout{background-color:#3f474e}.dark-mode .callout.callout-danger{border-left-color:#ed7669}.dark-mode .callout.callout-warning{border-left-color:#f5b043}.dark-mode .callout.callout-info{border-left-color:#5faee3}.dark-mode .callout.callout-success{border-left-color:#00efb2}.alert .icon{margin-right:10px}.alert .close,.alert .mailbox-attachment-close{color:#000;opacity:.2}.alert .close:hover,.alert .mailbox-attachment-close:hover{opacity:.5}.alert a{color:#fff;text-decoration:underline}.alert-primary{color:#fff;background-color:#007bff;border-color:#006fe6}.alert-default-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-default-primary hr{border-top-color:#9fcdff}.alert-default-primary .alert-link{color:#002752}.alert-secondary{color:#fff;background-color:#6c757d;border-color:#60686f}.alert-default-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-default-secondary hr{border-top-color:#c8cbcf}.alert-default-secondary .alert-link{color:#202326}.alert-success{color:#fff;background-color:#28a745;border-color:#23923d}.alert-default-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-default-success hr{border-top-color:#b1dfbb}.alert-default-success .alert-link{color:#0b2e13}.alert-info{color:#fff;background-color:#17a2b8;border-color:#148ea1}.alert-default-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-default-info hr{border-top-color:#abdde5}.alert-default-info .alert-link{color:#062c33}.alert-warning{color:#1f2d3d;background-color:#ffc107;border-color:#edb100}.alert-default-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-default-warning hr{border-top-color:#ffe8a1}.alert-default-warning .alert-link{color:#533f03}.alert-danger{color:#fff;background-color:#dc3545;border-color:#d32535}.alert-default-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-default-danger hr{border-top-color:#f1b0b7}.alert-default-danger .alert-link{color:#491217}.alert-light{color:#1f2d3d;background-color:#f8f9fa;border-color:#e9ecef}.alert-default-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-default-light hr{border-top-color:#ececf6}.alert-default-light .alert-link{color:#686868}.alert-dark{color:#fff;background-color:#343a40;border-color:#292d32}.alert-default-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-default-dark hr{border-top-color:#b9bbbe}.alert-default-dark .alert-link{color:#040505}.dark-mode .alert-primary{color:#fff;background-color:#3f6791;border-color:#375a7f}.dark-mode .alert-default-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.dark-mode .alert-default-primary hr{border-top-color:#9fcdff}.dark-mode .alert-default-primary .alert-link{color:#002752}.dark-mode .alert-secondary{color:#fff;background-color:#6c757d;border-color:#60686f}.dark-mode .alert-default-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.dark-mode .alert-default-secondary hr{border-top-color:#c8cbcf}.dark-mode .alert-default-secondary .alert-link{color:#202326}.dark-mode .alert-success{color:#fff;background-color:#00bc8c;border-color:#00a379}.dark-mode .alert-default-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.dark-mode .alert-default-success hr{border-top-color:#b1dfbb}.dark-mode .alert-default-success .alert-link{color:#0b2e13}.dark-mode .alert-info{color:#fff;background-color:#3498db;border-color:#258cd1}.dark-mode .alert-default-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.dark-mode .alert-default-info hr{border-top-color:#abdde5}.dark-mode .alert-default-info .alert-link{color:#062c33}.dark-mode .alert-warning{color:#1f2d3d;background-color:#f39c12;border-color:#e08e0b}.dark-mode .alert-default-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.dark-mode .alert-default-warning hr{border-top-color:#ffe8a1}.dark-mode .alert-default-warning .alert-link{color:#533f03}.dark-mode .alert-danger{color:#fff;background-color:#e74c3c;border-color:#e43725}.dark-mode .alert-default-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.dark-mode .alert-default-danger hr{border-top-color:#f1b0b7}.dark-mode .alert-default-danger .alert-link{color:#491217}.dark-mode .alert-light{color:#1f2d3d;background-color:#f8f9fa;border-color:#e9ecef}.dark-mode .alert-default-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.dark-mode .alert-default-light hr{border-top-color:#ececf6}.dark-mode .alert-default-light .alert-link{color:#686868}.dark-mode .alert-dark{color:#fff;background-color:#343a40;border-color:#292d32}.dark-mode .alert-default-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.dark-mode .alert-default-dark hr{border-top-color:#b9bbbe}.dark-mode .alert-default-dark .alert-link{color:#040505}.table:not(.table-dark){color:inherit}.table.table-head-fixed thead tr:nth-child(1) th{background-color:#fff;border-bottom:0;box-shadow:inset 0 1px 0 #dee2e6,inset 0 -1px 0 #dee2e6;position:-webkit-sticky;position:sticky;top:0;z-index:10}.table.table-head-fixed.table-dark thead tr:nth-child(1) th{background-color:#212529;box-shadow:inset 0 1px 0 #383f45,inset 0 -1px 0 #383f45}.table.no-border,.table.no-border td,.table.no-border th{border:0}.table.text-center,.table.text-center td,.table.text-center th{text-align:center}.table.table-valign-middle tbody>tr>td,.table.table-valign-middle tbody>tr>th,.table.table-valign-middle thead>tr>td,.table.table-valign-middle thead>tr>th{vertical-align:middle}.card-body.p-0 .table tbody>tr>td:first-of-type,.card-body.p-0 .table tbody>tr>th:first-of-type,.card-body.p-0 .table tfoot>tr>td:first-of-type,.card-body.p-0 .table tfoot>tr>th:first-of-type,.card-body.p-0 .table thead>tr>td:first-of-type,.card-body.p-0 .table thead>tr>th:first-of-type{padding-left:1.5rem}.card-body.p-0 .table tbody>tr>td:last-of-type,.card-body.p-0 .table tbody>tr>th:last-of-type,.card-body.p-0 .table tfoot>tr>td:last-of-type,.card-body.p-0 .table tfoot>tr>th:last-of-type,.card-body.p-0 .table thead>tr>td:last-of-type,.card-body.p-0 .table thead>tr>th:last-of-type{padding-right:1.5rem}.table-hover tbody tr.expandable-body:hover{background-color:inherit!important}[data-widget=expandable-table]{cursor:pointer}[data-widget=expandable-table] i.expandable-table-caret{transition:-webkit-transform .3s linear;transition:transform .3s linear;transition:transform .3s linear,-webkit-transform .3s linear}[data-widget=expandable-table][aria-expanded=true] td i.expandable-table-caret[class*=right]{-webkit-transform:rotate(90deg);transform:rotate(90deg)}[data-widget=expandable-table][aria-expanded=true] td i.expandable-table-caret[class*=left]{-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.expandable-body>td{padding:0!important;width:100%}.expandable-body>td>div,.expandable-body>td>p{padding:.75rem}.expandable-body .table{width:calc(100% - .75rem);margin:0 0 0 .75rem}.expandable-body .table tr:first-child td,.expandable-body .table tr:first-child th{border-top:none}.dark-mode .table-bordered,.dark-mode .table-bordered td,.dark-mode .table-bordered th{border-color:#6c757d}.dark-mode .table-hover tbody tr:hover{color:#dee2e6;background-color:#3a4047;border-color:#6c757d}.dark-mode .table thead th{border-bottom-color:#6c757d}.dark-mode .table td,.dark-mode .table th{border-top-color:#6c757d}.dark-mode .table.table-head-fixed thead tr:nth-child(1) th{background-color:#3f474e}.carousel-control-prev .carousel-control-custom-icon{margin-left:-20px}.carousel-control-next .carousel-control-custom-icon{margin-right:20px}.carousel-control-custom-icon>.fa,.carousel-control-custom-icon>.fab,.carousel-control-custom-icon>.fad,.carousel-control-custom-icon>.fal,.carousel-control-custom-icon>.far,.carousel-control-custom-icon>.fas,.carousel-control-custom-icon>.ion,.carousel-control-custom-icon>.svg-inline--fa{display:inline-block;font-size:40px;margin-top:-20px;position:absolute;top:50%;z-index:5}.close,.mailbox-attachment-close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover,.mailbox-attachment-close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover,.mailbox-attachment-close:not(:disabled):not(.disabled):focus,.mailbox-attachment-close:not(:disabled):not(.disabled):hover{opacity:.75}.close:focus,.mailbox-attachment-close:focus{outline:0}button.close,button.mailbox-attachment-close{padding:0;background-color:transparent;border:0}a.close.disabled,a.disabled.mailbox-attachment-close{pointer-events:none}.small-box{border-radius:.25rem;box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);display:block;margin-bottom:20px;position:relative}.small-box>.inner{padding:10px}.small-box>.small-box-footer{background-color:rgba(0,0,0,.1);color:rgba(255,255,255,.8);display:block;padding:3px 0;position:relative;text-align:center;text-decoration:none;z-index:10}.small-box>.small-box-footer:hover{background-color:rgba(0,0,0,.15);color:#fff}.small-box h3{font-size:2.2rem;font-weight:700;margin:0 0 10px;padding:0;white-space:nowrap}@media (min-width:992px){.col-lg-2 .small-box h3,.col-md-2 .small-box h3,.col-xl-2 .small-box h3{font-size:1.6rem}.col-lg-3 .small-box h3,.col-md-3 .small-box h3,.col-xl-3 .small-box h3{font-size:1.6rem}}@media (min-width:1200px){.col-lg-2 .small-box h3,.col-md-2 .small-box h3,.col-xl-2 .small-box h3{font-size:2.2rem}.col-lg-3 .small-box h3,.col-md-3 .small-box h3,.col-xl-3 .small-box h3{font-size:2.2rem}}.small-box p{font-size:1rem}.small-box p>small{color:#f8f9fa;display:block;font-size:.9rem;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{color:rgba(0,0,0,.15);z-index:0}.small-box .icon>i{font-size:90px;position:absolute;right:15px;top:15px;transition:-webkit-transform .3s linear;transition:transform .3s linear;transition:transform .3s linear,-webkit-transform .3s linear}.small-box .icon>i.fa,.small-box .icon>i.fab,.small-box .icon>i.fad,.small-box .icon>i.fal,.small-box .icon>i.far,.small-box .icon>i.fas,.small-box .icon>i.ion{font-size:70px;top:20px}.small-box .icon svg{font-size:70px;position:absolute;right:15px;top:15px;transition:-webkit-transform .3s linear;transition:transform .3s linear;transition:transform .3s linear,-webkit-transform .3s linear}.small-box:hover{text-decoration:none}.small-box:hover .icon>i,.small-box:hover .icon>i.fa,.small-box:hover .icon>i.fab,.small-box:hover .icon>i.fad,.small-box:hover .icon>i.fal,.small-box:hover .icon>i.far,.small-box:hover .icon>i.fas,.small-box:hover .icon>i.ion{-webkit-transform:scale(1.1);transform:scale(1.1)}.small-box:hover .icon>svg{-webkit-transform:scale(1.1);transform:scale(1.1)}@media (max-width:767.98px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.info-box{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);border-radius:.25rem;background-color:#fff;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:1rem;min-height:80px;padding:.5rem;position:relative;width:100%}.info-box .progress{background-color:rgba(0,0,0,.125);height:2px;margin:5px 0}.info-box .progress .progress-bar{background-color:#fff}.info-box .info-box-icon{border-radius:.25rem;-webkit-align-items:center;-ms-flex-align:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:1.875rem;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center;width:70px}.info-box .info-box-icon>img{max-width:100%}.info-box .info-box-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;line-height:1.8;-webkit-flex:1;-ms-flex:1;flex:1;padding:0 10px}.info-box .info-box-number{display:block;margin-top:.25rem;font-weight:700}.info-box .info-box-text,.info-box .progress-description{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.info-box .info-box .bg-gradient-primary,.info-box .info-box .bg-primary{color:#fff}.info-box .info-box .bg-gradient-primary .progress-bar,.info-box .info-box .bg-primary .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-secondary,.info-box .info-box .bg-secondary{color:#fff}.info-box .info-box .bg-gradient-secondary .progress-bar,.info-box .info-box .bg-secondary .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-success,.info-box .info-box .bg-success{color:#fff}.info-box .info-box .bg-gradient-success .progress-bar,.info-box .info-box .bg-success .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-info,.info-box .info-box .bg-info{color:#fff}.info-box .info-box .bg-gradient-info .progress-bar,.info-box .info-box .bg-info .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-warning,.info-box .info-box .bg-warning{color:#1f2d3d}.info-box .info-box .bg-gradient-warning .progress-bar,.info-box .info-box .bg-warning .progress-bar{background-color:#1f2d3d}.info-box .info-box .bg-danger,.info-box .info-box .bg-gradient-danger{color:#fff}.info-box .info-box .bg-danger .progress-bar,.info-box .info-box .bg-gradient-danger .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-light,.info-box .info-box .bg-light{color:#1f2d3d}.info-box .info-box .bg-gradient-light .progress-bar,.info-box .info-box .bg-light .progress-bar{background-color:#1f2d3d}.info-box .info-box .bg-dark,.info-box .info-box .bg-gradient-dark{color:#fff}.info-box .info-box .bg-dark .progress-bar,.info-box .info-box .bg-gradient-dark .progress-bar{background-color:#fff}.info-box .info-box-more{display:block}.info-box .progress-description{margin:0}@media (min-width:768px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{display:none}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{display:none}}@media (min-width:992px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:.75rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:.75rem;display:block}}@media (min-width:1200px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:1rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:1rem;display:block}}.dark-mode .info-box{background-color:#343a40;color:#fff}.dark-mode .info-box .info-box .bg-gradient-primary,.dark-mode .info-box .info-box .bg-primary{color:#fff}.dark-mode .info-box .info-box .bg-gradient-primary .progress-bar,.dark-mode .info-box .info-box .bg-primary .progress-bar{background-color:#fff}.dark-mode .info-box .info-box .bg-gradient-secondary,.dark-mode .info-box .info-box .bg-secondary{color:#fff}.dark-mode .info-box .info-box .bg-gradient-secondary .progress-bar,.dark-mode .info-box .info-box .bg-secondary .progress-bar{background-color:#fff}.dark-mode .info-box .info-box .bg-gradient-success,.dark-mode .info-box .info-box .bg-success{color:#fff}.dark-mode .info-box .info-box .bg-gradient-success .progress-bar,.dark-mode .info-box .info-box .bg-success .progress-bar{background-color:#fff}.dark-mode .info-box .info-box .bg-gradient-info,.dark-mode .info-box .info-box .bg-info{color:#fff}.dark-mode .info-box .info-box .bg-gradient-info .progress-bar,.dark-mode .info-box .info-box .bg-info .progress-bar{background-color:#fff}.dark-mode .info-box .info-box .bg-gradient-warning,.dark-mode .info-box .info-box .bg-warning{color:#1f2d3d}.dark-mode .info-box .info-box .bg-gradient-warning .progress-bar,.dark-mode .info-box .info-box .bg-warning .progress-bar{background-color:#1f2d3d}.dark-mode .info-box .info-box .bg-danger,.dark-mode .info-box .info-box .bg-gradient-danger{color:#fff}.dark-mode .info-box .info-box .bg-danger .progress-bar,.dark-mode .info-box .info-box .bg-gradient-danger .progress-bar{background-color:#fff}.dark-mode .info-box .info-box .bg-gradient-light,.dark-mode .info-box .info-box .bg-light{color:#1f2d3d}.dark-mode .info-box .info-box .bg-gradient-light .progress-bar,.dark-mode .info-box .info-box .bg-light .progress-bar{background-color:#1f2d3d}.dark-mode .info-box .info-box .bg-dark,.dark-mode .info-box .info-box .bg-gradient-dark{color:#fff}.dark-mode .info-box .info-box .bg-dark .progress-bar,.dark-mode .info-box .info-box .bg-gradient-dark .progress-bar{background-color:#fff}.timeline{margin:0 0 45px;padding:0;position:relative}.timeline::before{border-radius:.25rem;background-color:#dee2e6;bottom:0;content:"";left:31px;margin:0;position:absolute;top:0;width:4px}.timeline>div{margin-bottom:15px;margin-right:10px;position:relative}.timeline>div::after,.timeline>div::before{content:"";display:table}.timeline>div>.timeline-item{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);border-radius:.25rem;background-color:#fff;color:#495057;margin-left:60px;margin-right:15px;margin-top:0;padding:0;position:relative}.timeline>div>.timeline-item>.time{color:#999;float:right;font-size:12px;padding:10px}.timeline>div>.timeline-item>.timeline-header{border-bottom:1px solid rgba(0,0,0,.125);color:#495057;font-size:16px;line-height:1.1;margin:0;padding:10px}.timeline>div>.timeline-item>.timeline-header>a{font-weight:600}.timeline>div>.timeline-item>.timeline-body,.timeline>div>.timeline-item>.timeline-footer{padding:10px}.timeline>div>.timeline-item>.timeline-body>img{margin:10px}.timeline>div>.timeline-item>.timeline-body ol,.timeline>div>.timeline-item>.timeline-body ul,.timeline>div>.timeline-item>.timeline-body>dl{margin:0}.timeline>div>.timeline-item>.timeline-footer>a{color:#fff}.timeline>div>.fa,.timeline>div>.fab,.timeline>div>.fad,.timeline>div>.fal,.timeline>div>.far,.timeline>div>.fas,.timeline>div>.ion,.timeline>div>.svg-inline--fa{background-color:#adb5bd;border-radius:50%;font-size:16px;height:30px;left:18px;line-height:30px;position:absolute;text-align:center;top:0;width:30px}.timeline>div>.svg-inline--fa{padding:7px}.timeline>.time-label>span{border-radius:4px;background-color:#fff;display:inline-block;font-weight:600;padding:5px}.timeline-inverse>div>.timeline-item{box-shadow:none;background-color:#f8f9fa;border:1px solid #dee2e6}.timeline-inverse>div>.timeline-item>.timeline-header{border-bottom-color:#dee2e6}.dark-mode .timeline::before{background-color:#6c757d}.dark-mode .timeline>div>.timeline-item{background-color:#343a40;color:#fff;border-color:#6c757d}.dark-mode .timeline>div>.timeline-item>.timeline-header{color:#ced4da;border-color:#6c757d}.dark-mode .timeline>div>.timeline-item>.time{color:#ced4da}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:.25rem;background-color:#fff;padding:10px 0}.products-list>.item::after{display:block;clear:both;content:""}.products-list .product-img{float:left}.products-list .product-img img{height:50px;width:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{color:#6c757d;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.product-list-in-card>.item{border-radius:0;border-bottom:1px solid rgba(0,0,0,.125)}.product-list-in-card>.item:last-of-type{border-bottom-width:0}.dark-mode .products-list>.item{background-color:#343a40;color:#fff;border-bottom-color:#6c757d}.dark-mode .product-description{color:#ced4da}.direct-chat .card-body{overflow-x:hidden;padding:0;position:relative}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0,0);transform:translate(0,0)}.direct-chat.timestamp-light .direct-chat-timestamp{color:#30465f}.direct-chat.timestamp-dark .direct-chat-timestamp{color:#ccc}.direct-chat-messages{-webkit-transform:translate(0,0);transform:translate(0,0);height:250px;overflow:auto;padding:10px}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg::after{display:block;clear:both;content:""}.direct-chat-contacts,.direct-chat-messages{transition:-webkit-transform .5s ease-in-out;transition:transform .5s ease-in-out;transition:transform .5s ease-in-out,-webkit-transform .5s ease-in-out}.direct-chat-text{border-radius:.3rem;background-color:#d2d6de;border:1px solid #d2d6de;color:#444;margin:5px 0 0 50px;padding:5px 10px;position:relative}.direct-chat-text::after,.direct-chat-text::before{border:solid transparent;border-right-color:#d2d6de;content:" ";height:0;pointer-events:none;position:absolute;right:100%;top:15px;width:0}.direct-chat-text::after{border-width:5px;margin-top:-5px}.direct-chat-text::before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-left:0;margin-right:50px}.right .direct-chat-text::after,.right .direct-chat-text::before{border-left-color:#d2d6de;border-right-color:transparent;left:100%;right:auto}.direct-chat-img{border-radius:50%;float:left;height:40px;width:40px}.right .direct-chat-img{float:right}.direct-chat-infos{display:block;font-size:.875rem;margin-bottom:2px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#697582}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0,0);transform:translate(0,0)}.direct-chat-contacts{-webkit-transform:translate(101%,0);transform:translate(101%,0);background-color:#343a40;bottom:0;color:#fff;height:250px;overflow:auto;position:absolute;top:0;width:100%}.direct-chat-contacts-light{background-color:#f8f9fa}.direct-chat-contacts-light .contacts-list-name{color:#495057}.direct-chat-contacts-light .contacts-list-date{color:#6c757d}.direct-chat-contacts-light .contacts-list-msg{color:#545b62}.contacts-list{padding-left:0;list-style:none}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,.2);margin:0;padding:10px}.contacts-list>li::after{display:block;clear:both;content:""}.contacts-list>li:last-of-type{border-bottom:0}.contacts-list-img{border-radius:50%;float:left;width:40px}.contacts-list-info{color:#fff;margin-left:45px}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:.875rem}.contacts-list-date{color:#ced4da;font-weight:400}.contacts-list-msg{color:#b1bbc4}.direct-chat-primary .right>.direct-chat-text{background-color:#007bff;border-color:#007bff;color:#fff}.direct-chat-primary .right>.direct-chat-text::after,.direct-chat-primary .right>.direct-chat-text::before{border-left-color:#007bff}.direct-chat-secondary .right>.direct-chat-text{background-color:#6c757d;border-color:#6c757d;color:#fff}.direct-chat-secondary .right>.direct-chat-text::after,.direct-chat-secondary .right>.direct-chat-text::before{border-left-color:#6c757d}.direct-chat-success .right>.direct-chat-text{background-color:#28a745;border-color:#28a745;color:#fff}.direct-chat-success .right>.direct-chat-text::after,.direct-chat-success .right>.direct-chat-text::before{border-left-color:#28a745}.direct-chat-info .right>.direct-chat-text{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.direct-chat-info .right>.direct-chat-text::after,.direct-chat-info .right>.direct-chat-text::before{border-left-color:#17a2b8}.direct-chat-warning .right>.direct-chat-text{background-color:#ffc107;border-color:#ffc107;color:#1f2d3d}.direct-chat-warning .right>.direct-chat-text::after,.direct-chat-warning .right>.direct-chat-text::before{border-left-color:#ffc107}.direct-chat-danger .right>.direct-chat-text{background-color:#dc3545;border-color:#dc3545;color:#fff}.direct-chat-danger .right>.direct-chat-text::after,.direct-chat-danger .right>.direct-chat-text::before{border-left-color:#dc3545}.direct-chat-light .right>.direct-chat-text{background-color:#f8f9fa;border-color:#f8f9fa;color:#1f2d3d}.direct-chat-light .right>.direct-chat-text::after,.direct-chat-light .right>.direct-chat-text::before{border-left-color:#f8f9fa}.direct-chat-dark .right>.direct-chat-text{background-color:#343a40;border-color:#343a40;color:#fff}.direct-chat-dark .right>.direct-chat-text::after,.direct-chat-dark .right>.direct-chat-text::before{border-left-color:#343a40}.direct-chat-lightblue .right>.direct-chat-text{background-color:#3c8dbc;border-color:#3c8dbc;color:#fff}.direct-chat-lightblue .right>.direct-chat-text::after,.direct-chat-lightblue .right>.direct-chat-text::before{border-left-color:#3c8dbc}.direct-chat-navy .right>.direct-chat-text{background-color:#001f3f;border-color:#001f3f;color:#fff}.direct-chat-navy .right>.direct-chat-text::after,.direct-chat-navy .right>.direct-chat-text::before{border-left-color:#001f3f}.direct-chat-olive .right>.direct-chat-text{background-color:#3d9970;border-color:#3d9970;color:#fff}.direct-chat-olive .right>.direct-chat-text::after,.direct-chat-olive .right>.direct-chat-text::before{border-left-color:#3d9970}.direct-chat-lime .right>.direct-chat-text{background-color:#01ff70;border-color:#01ff70;color:#1f2d3d}.direct-chat-lime .right>.direct-chat-text::after,.direct-chat-lime .right>.direct-chat-text::before{border-left-color:#01ff70}.direct-chat-fuchsia .right>.direct-chat-text{background-color:#f012be;border-color:#f012be;color:#fff}.direct-chat-fuchsia .right>.direct-chat-text::after,.direct-chat-fuchsia .right>.direct-chat-text::before{border-left-color:#f012be}.direct-chat-maroon .right>.direct-chat-text{background-color:#d81b60;border-color:#d81b60;color:#fff}.direct-chat-maroon .right>.direct-chat-text::after,.direct-chat-maroon .right>.direct-chat-text::before{border-left-color:#d81b60}.direct-chat-blue .right>.direct-chat-text{background-color:#007bff;border-color:#007bff;color:#fff}.direct-chat-blue .right>.direct-chat-text::after,.direct-chat-blue .right>.direct-chat-text::before{border-left-color:#007bff}.direct-chat-indigo .right>.direct-chat-text{background-color:#6610f2;border-color:#6610f2;color:#fff}.direct-chat-indigo .right>.direct-chat-text::after,.direct-chat-indigo .right>.direct-chat-text::before{border-left-color:#6610f2}.direct-chat-purple .right>.direct-chat-text{background-color:#6f42c1;border-color:#6f42c1;color:#fff}.direct-chat-purple .right>.direct-chat-text::after,.direct-chat-purple .right>.direct-chat-text::before{border-left-color:#6f42c1}.direct-chat-pink .right>.direct-chat-text{background-color:#e83e8c;border-color:#e83e8c;color:#fff}.direct-chat-pink .right>.direct-chat-text::after,.direct-chat-pink .right>.direct-chat-text::before{border-left-color:#e83e8c}.direct-chat-red .right>.direct-chat-text{background-color:#dc3545;border-color:#dc3545;color:#fff}.direct-chat-red .right>.direct-chat-text::after,.direct-chat-red .right>.direct-chat-text::before{border-left-color:#dc3545}.direct-chat-orange .right>.direct-chat-text{background-color:#fd7e14;border-color:#fd7e14;color:#1f2d3d}.direct-chat-orange .right>.direct-chat-text::after,.direct-chat-orange .right>.direct-chat-text::before{border-left-color:#fd7e14}.direct-chat-yellow .right>.direct-chat-text{background-color:#ffc107;border-color:#ffc107;color:#1f2d3d}.direct-chat-yellow .right>.direct-chat-text::after,.direct-chat-yellow .right>.direct-chat-text::before{border-left-color:#ffc107}.direct-chat-green .right>.direct-chat-text{background-color:#28a745;border-color:#28a745;color:#fff}.direct-chat-green .right>.direct-chat-text::after,.direct-chat-green .right>.direct-chat-text::before{border-left-color:#28a745}.direct-chat-teal .right>.direct-chat-text{background-color:#20c997;border-color:#20c997;color:#fff}.direct-chat-teal .right>.direct-chat-text::after,.direct-chat-teal .right>.direct-chat-text::before{border-left-color:#20c997}.direct-chat-cyan .right>.direct-chat-text{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.direct-chat-cyan .right>.direct-chat-text::after,.direct-chat-cyan .right>.direct-chat-text::before{border-left-color:#17a2b8}.direct-chat-white .right>.direct-chat-text{background-color:#fff;border-color:#fff;color:#1f2d3d}.direct-chat-white .right>.direct-chat-text::after,.direct-chat-white .right>.direct-chat-text::before{border-left-color:#fff}.direct-chat-gray .right>.direct-chat-text{background-color:#6c757d;border-color:#6c757d;color:#fff}.direct-chat-gray .right>.direct-chat-text::after,.direct-chat-gray .right>.direct-chat-text::before{border-left-color:#6c757d}.direct-chat-gray-dark .right>.direct-chat-text{background-color:#343a40;border-color:#343a40;color:#fff}.direct-chat-gray-dark .right>.direct-chat-text::after,.direct-chat-gray-dark .right>.direct-chat-text::before{border-left-color:#343a40}.dark-mode .direct-chat-text{background-color:#454d55;border-color:#4b545c;color:#fff}.dark-mode .direct-chat-text::after,.dark-mode .direct-chat-text::before{border-right-color:#4b545c}.dark-mode .direct-chat-timestamp{color:#adb5bd}.dark-mode .right>.direct-chat-text::after,.dark-mode .right>.direct-chat-text::before{border-right-color:transparent}.dark-mode .direct-chat-primary .right>.direct-chat-text{background-color:#3f6791;border-color:#3f6791;color:#fff}.dark-mode .direct-chat-primary .right>.direct-chat-text::after,.dark-mode .direct-chat-primary .right>.direct-chat-text::before{border-left-color:#3f6791}.dark-mode .direct-chat-secondary .right>.direct-chat-text{background-color:#6c757d;border-color:#6c757d;color:#fff}.dark-mode .direct-chat-secondary .right>.direct-chat-text::after,.dark-mode .direct-chat-secondary .right>.direct-chat-text::before{border-left-color:#6c757d}.dark-mode .direct-chat-success .right>.direct-chat-text{background-color:#00bc8c;border-color:#00bc8c;color:#fff}.dark-mode .direct-chat-success .right>.direct-chat-text::after,.dark-mode .direct-chat-success .right>.direct-chat-text::before{border-left-color:#00bc8c}.dark-mode .direct-chat-info .right>.direct-chat-text{background-color:#3498db;border-color:#3498db;color:#fff}.dark-mode .direct-chat-info .right>.direct-chat-text::after,.dark-mode .direct-chat-info .right>.direct-chat-text::before{border-left-color:#3498db}.dark-mode .direct-chat-warning .right>.direct-chat-text{background-color:#f39c12;border-color:#f39c12;color:#1f2d3d}.dark-mode .direct-chat-warning .right>.direct-chat-text::after,.dark-mode .direct-chat-warning .right>.direct-chat-text::before{border-left-color:#f39c12}.dark-mode .direct-chat-danger .right>.direct-chat-text{background-color:#e74c3c;border-color:#e74c3c;color:#fff}.dark-mode .direct-chat-danger .right>.direct-chat-text::after,.dark-mode .direct-chat-danger .right>.direct-chat-text::before{border-left-color:#e74c3c}.dark-mode .direct-chat-light .right>.direct-chat-text{background-color:#f8f9fa;border-color:#f8f9fa;color:#1f2d3d}.dark-mode .direct-chat-light .right>.direct-chat-text::after,.dark-mode .direct-chat-light .right>.direct-chat-text::before{border-left-color:#f8f9fa}.dark-mode .direct-chat-dark .right>.direct-chat-text{background-color:#343a40;border-color:#343a40;color:#fff}.dark-mode .direct-chat-dark .right>.direct-chat-text::after,.dark-mode .direct-chat-dark .right>.direct-chat-text::before{border-left-color:#343a40}.dark-mode .direct-chat-lightblue .right>.direct-chat-text{background-color:#86bad8;border-color:#86bad8;color:#1f2d3d}.dark-mode .direct-chat-lightblue .right>.direct-chat-text::after,.dark-mode .direct-chat-lightblue .right>.direct-chat-text::before{border-left-color:#86bad8}.dark-mode .direct-chat-navy .right>.direct-chat-text{background-color:#002c59;border-color:#002c59;color:#fff}.dark-mode .direct-chat-navy .right>.direct-chat-text::after,.dark-mode .direct-chat-navy .right>.direct-chat-text::before{border-left-color:#002c59}.dark-mode .direct-chat-olive .right>.direct-chat-text{background-color:#74c8a3;border-color:#74c8a3;color:#1f2d3d}.dark-mode .direct-chat-olive .right>.direct-chat-text::after,.dark-mode .direct-chat-olive .right>.direct-chat-text::before{border-left-color:#74c8a3}.dark-mode .direct-chat-lime .right>.direct-chat-text{background-color:#67ffa9;border-color:#67ffa9;color:#1f2d3d}.dark-mode .direct-chat-lime .right>.direct-chat-text::after,.dark-mode .direct-chat-lime .right>.direct-chat-text::before{border-left-color:#67ffa9}.dark-mode .direct-chat-fuchsia .right>.direct-chat-text{background-color:#f672d8;border-color:#f672d8;color:#1f2d3d}.dark-mode .direct-chat-fuchsia .right>.direct-chat-text::after,.dark-mode .direct-chat-fuchsia .right>.direct-chat-text::before{border-left-color:#f672d8}.dark-mode .direct-chat-maroon .right>.direct-chat-text{background-color:#ed6c9b;border-color:#ed6c9b;color:#1f2d3d}.dark-mode .direct-chat-maroon .right>.direct-chat-text::after,.dark-mode .direct-chat-maroon .right>.direct-chat-text::before{border-left-color:#ed6c9b}.dark-mode .direct-chat-blue .right>.direct-chat-text{background-color:#3f6791;border-color:#3f6791;color:#fff}.dark-mode .direct-chat-blue .right>.direct-chat-text::after,.dark-mode .direct-chat-blue .right>.direct-chat-text::before{border-left-color:#3f6791}.dark-mode .direct-chat-indigo .right>.direct-chat-text{background-color:#6610f2;border-color:#6610f2;color:#fff}.dark-mode .direct-chat-indigo .right>.direct-chat-text::after,.dark-mode .direct-chat-indigo .right>.direct-chat-text::before{border-left-color:#6610f2}.dark-mode .direct-chat-purple .right>.direct-chat-text{background-color:#6f42c1;border-color:#6f42c1;color:#fff}.dark-mode .direct-chat-purple .right>.direct-chat-text::after,.dark-mode .direct-chat-purple .right>.direct-chat-text::before{border-left-color:#6f42c1}.dark-mode .direct-chat-pink .right>.direct-chat-text{background-color:#e83e8c;border-color:#e83e8c;color:#fff}.dark-mode .direct-chat-pink .right>.direct-chat-text::after,.dark-mode .direct-chat-pink .right>.direct-chat-text::before{border-left-color:#e83e8c}.dark-mode .direct-chat-red .right>.direct-chat-text{background-color:#e74c3c;border-color:#e74c3c;color:#fff}.dark-mode .direct-chat-red .right>.direct-chat-text::after,.dark-mode .direct-chat-red .right>.direct-chat-text::before{border-left-color:#e74c3c}.dark-mode .direct-chat-orange .right>.direct-chat-text{background-color:#fd7e14;border-color:#fd7e14;color:#1f2d3d}.dark-mode .direct-chat-orange .right>.direct-chat-text::after,.dark-mode .direct-chat-orange .right>.direct-chat-text::before{border-left-color:#fd7e14}.dark-mode .direct-chat-yellow .right>.direct-chat-text{background-color:#f39c12;border-color:#f39c12;color:#1f2d3d}.dark-mode .direct-chat-yellow .right>.direct-chat-text::after,.dark-mode .direct-chat-yellow .right>.direct-chat-text::before{border-left-color:#f39c12}.dark-mode .direct-chat-green .right>.direct-chat-text{background-color:#00bc8c;border-color:#00bc8c;color:#fff}.dark-mode .direct-chat-green .right>.direct-chat-text::after,.dark-mode .direct-chat-green .right>.direct-chat-text::before{border-left-color:#00bc8c}.dark-mode .direct-chat-teal .right>.direct-chat-text{background-color:#20c997;border-color:#20c997;color:#fff}.dark-mode .direct-chat-teal .right>.direct-chat-text::after,.dark-mode .direct-chat-teal .right>.direct-chat-text::before{border-left-color:#20c997}.dark-mode .direct-chat-cyan .right>.direct-chat-text{background-color:#3498db;border-color:#3498db;color:#fff}.dark-mode .direct-chat-cyan .right>.direct-chat-text::after,.dark-mode .direct-chat-cyan .right>.direct-chat-text::before{border-left-color:#3498db}.dark-mode .direct-chat-white .right>.direct-chat-text{background-color:#fff;border-color:#fff;color:#1f2d3d}.dark-mode .direct-chat-white .right>.direct-chat-text::after,.dark-mode .direct-chat-white .right>.direct-chat-text::before{border-left-color:#fff}.dark-mode .direct-chat-gray .right>.direct-chat-text{background-color:#6c757d;border-color:#6c757d;color:#fff}.dark-mode .direct-chat-gray .right>.direct-chat-text::after,.dark-mode .direct-chat-gray .right>.direct-chat-text::before{border-left-color:#6c757d}.dark-mode .direct-chat-gray-dark .right>.direct-chat-text{background-color:#343a40;border-color:#343a40;color:#fff}.dark-mode .direct-chat-gray-dark .right>.direct-chat-text::after,.dark-mode .direct-chat-gray-dark .right>.direct-chat-text::before{border-left-color:#343a40}.users-list{padding-left:0;list-style:none}.users-list>li{float:left;padding:10px;text-align:center;width:25%}.users-list>li img{border-radius:50%;height:auto;max-width:100%}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-date,.users-list-name{display:block}.users-list-name{color:#495057;font-size:.875rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.users-list-date{color:#748290;font-size:12px}.dark-mode .users-list-name{color:#ced4da}.dark-mode .users-list-date{color:#adb5bd}.card-widget{border:0;position:relative}.widget-user .widget-user-header{border-top-left-radius:.25rem;border-top-right-radius:.25rem;height:135px;padding:1rem;text-align:center}.widget-user .widget-user-username{font-size:25px;font-weight:300;margin-bottom:0;margin-top:0;text-shadow:0 1px 1px rgba(0,0,0,.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{left:50%;margin-left:-45px;position:absolute;top:80px}.widget-user .widget-user-image>img{border:3px solid #fff;height:auto;width:90px}.widget-user .card-footer{padding-top:50px}.widget-user-2 .widget-user-header{border-top-left-radius:.25rem;border-top-right-radius:.25rem;padding:1rem}.widget-user-2 .widget-user-username{font-size:25px;font-weight:300;margin-bottom:5px;margin-top:5px}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-desc,.widget-user-2 .widget-user-username{margin-left:75px}.widget-user-2 .widget-user-image>img{float:left;height:auto;width:65px}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid rgba(0,0,0,.125)}.mailbox-read-info{border-bottom:1px solid rgba(0,0,0,.125);padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments{padding-left:0;list-style:none}.mailbox-attachments li{border:1px solid #eee;float:left;margin-bottom:10px;margin-right:10px;width:200px}.mailbox-attachment-name{color:#666;font-weight:700}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{background-color:#f8f9fa;padding:10px}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-size>span{display:inline-block;padding-top:.75rem}.mailbox-attachment-icon{color:#666;font-size:65px;max-height:132.5px;padding:20px 10px;text-align:center}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{height:auto;max-width:100%}.lockscreen{background-color:#e9ecef}.lockscreen .lockscreen-name{font-weight:600;text-align:center}.lockscreen-logo{font-size:35px;font-weight:300;margin-bottom:25px;text-align:center}.lockscreen-logo a{color:#495057}.lockscreen-wrapper{margin:0 auto;margin-top:10%;max-width:400px}.lockscreen-item{border-radius:4px;background-color:#fff;margin:10px auto 30px;padding:0;position:relative;width:290px}.lockscreen-image{border-radius:50%;background-color:#fff;left:-10px;padding:5px;position:absolute;top:-25px;z-index:10}.lockscreen-image>img{border-radius:50%;height:70px;width:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.dark-mode .lockscreen-item{background-color:#343a40}.dark-mode .lockscreen-logo a{color:#fff}.dark-mode .lockscreen-credentials .btn{background-color:#343a40}.dark-mode .lockscreen-image{background-color:#6c757d}.login-logo,.register-logo{font-size:2.1rem;font-weight:300;margin-bottom:.9rem;text-align:center}.login-logo a,.register-logo a{color:#495057}.login-page,.register-page{-webkit-align-items:center;-ms-flex-align:center;align-items:center;background-color:#e9ecef;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;height:100vh;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.login-box,.register-box{width:360px}@media (max-width:576px){.login-box,.register-box{margin-top:.5rem;width:90%}}.login-box .card,.register-box .card{margin-bottom:0}.login-card-body,.register-card-body{background-color:#fff;border-top:0;color:#666;padding:20px}.login-card-body .input-group .form-control,.register-card-body .input-group .form-control{border-right:0}.login-card-body .input-group .form-control:focus,.register-card-body .input-group .form-control:focus{box-shadow:none}.login-card-body .input-group .form-control:focus~.input-group-append .input-group-text,.login-card-body .input-group .form-control:focus~.input-group-prepend .input-group-text,.register-card-body .input-group .form-control:focus~.input-group-append .input-group-text,.register-card-body .input-group .form-control:focus~.input-group-prepend .input-group-text{border-color:#80bdff}.login-card-body .input-group .form-control.is-valid:focus,.register-card-body .input-group .form-control.is-valid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text,.login-card-body .input-group .form-control.is-valid~.input-group-prepend .input-group-text,.register-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-valid~.input-group-prepend .input-group-text{border-color:#28a745}.login-card-body .input-group .form-control.is-invalid:focus,.register-card-body .input-group .form-control.is-invalid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text{border-color:#dc3545}.login-card-body .input-group .input-group-text,.register-card-body .input-group .input-group-text{background-color:transparent;border-bottom-right-radius:.25rem;border-left:0;border-top-right-radius:.25rem;color:#777;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.login-box-msg,.register-box-msg{margin:0;padding:0 20px 20px;text-align:center}.social-auth-links{margin:10px 0}.dark-mode .login-card-body,.dark-mode .register-card-body{background-color:#343a40;border-color:#6c757d;color:#fff}.dark-mode .login-logo a,.dark-mode .register-logo a{color:#fff}.error-page{margin:20px auto 0;width:600px}@media (max-width:767.98px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:767.98px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{display:block;margin-left:190px}@media (max-width:767.98px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-size:25px;font-weight:300}@media (max-width:767.98px){.error-page>.error-content>h3{text-align:center}}.invoice{background-color:#fff;border:1px solid rgba(0,0,0,.125);position:relative}.invoice-title{margin-top:0}.dark-mode .invoice{background-color:#343a40}.profile-user-img{border:3px solid #adb5bd;margin:0 auto;padding:3px;width:100px}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #adb5bd;color:#666;margin-bottom:15px;padding-bottom:15px}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px;width:100%}.post .row{width:100%}.dark-mode .post{color:#fff;border-color:#6c757d}.product-image{max-width:100%;height:auto;width:100%}.product-image-thumbs{-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:2rem}.product-image-thumb{box-shadow:0 1px 2px rgba(0,0,0,.075);border-radius:.25rem;background-color:#fff;border:1px solid #dee2e6;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-right:1rem;max-width:7rem;padding:.5rem}.product-image-thumb img{max-width:100%;height:auto;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.product-image-thumb:hover{opacity:.5}.product-share a{margin-right:.5rem}.projects td{vertical-align:middle}.projects .list-inline{margin-bottom:0}.projects .table-avatar img,.projects img.table-avatar{border-radius:50%;display:inline;width:2.5rem}.projects .project-state{text-align:center}body.iframe-mode .main-sidebar{display:none}body.iframe-mode .content-wrapper{margin-left:0!important;margin-top:0!important;padding-bottom:0!important}body.iframe-mode .main-footer,body.iframe-mode .main-header{display:none}body.iframe-mode-fullscreen{overflow:hidden}.content-wrapper{height:100%}.content-wrapper.iframe-mode .btn-iframe-close{color:#dc3545;position:absolute;line-height:1;right:.125rem;top:.125rem;z-index:10;visibility:hidden}.content-wrapper.iframe-mode .btn-iframe-close:focus,.content-wrapper.iframe-mode .btn-iframe-close:hover{-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}@media (hover:none) and (pointer:coarse){.content-wrapper.iframe-mode .btn-iframe-close{visibility:visible}}.content-wrapper.iframe-mode .navbar-nav{overflow-y:auto;width:100%}.content-wrapper.iframe-mode .navbar-nav .nav-link{white-space:nowrap}.content-wrapper.iframe-mode .navbar-nav .nav-item{position:relative}.content-wrapper.iframe-mode .navbar-nav .nav-item:focus .btn-iframe-close,.content-wrapper.iframe-mode .navbar-nav .nav-item:hover .btn-iframe-close{-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;visibility:visible}@media (hover:none) and (pointer:coarse){.content-wrapper.iframe-mode .navbar-nav .nav-item:focus .btn-iframe-close,.content-wrapper.iframe-mode .navbar-nav .nav-item:hover .btn-iframe-close{visibility:visible}}.content-wrapper.iframe-mode .tab-content{position:relative}.content-wrapper.iframe-mode .tab-pane+.tab-empty{display:none}.content-wrapper.iframe-mode .tab-empty{width:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.content-wrapper.iframe-mode .tab-loading{position:absolute;top:0;left:0;width:100%;display:none;background-color:#f4f6f9}.content-wrapper.iframe-mode .tab-loading>div{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;width:100%;height:100%}.content-wrapper.iframe-mode iframe{border:0;width:100%;height:100%;margin-bottom:-8px}.content-wrapper.iframe-mode iframe .content-wrapper{padding-bottom:0!important}body.iframe-mode-fullscreen .content-wrapper.iframe-mode{position:absolute;left:0;top:0;right:0;bottom:0;margin-left:0!important;height:100%;min-height:100%;z-index:1048}.permanent-btn-iframe-close .btn-iframe-close{-webkit-animation:none!important;animation:none!important;visibility:visible!important;opacity:1}.content-wrapper.kanban{height:1px}.content-wrapper.kanban .content{height:100%;overflow-x:auto;overflow-y:hidden}.content-wrapper.kanban .content .container,.content-wrapper.kanban .content .container-fluid,.content-wrapper.kanban .content .container-lg,.content-wrapper.kanban .content .container-md,.content-wrapper.kanban .content .container-sm,.content-wrapper.kanban .content .container-xl{width:-webkit-max-content;width:-moz-max-content;width:max-content;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.content-wrapper.kanban .content-header+.content{height:calc(100% - ((2 * 15px) + (1.8rem * 1.2)))}.content-wrapper.kanban .card .card-body{padding:.5rem}.content-wrapper.kanban .card.card-row{width:340px;display:inline-block;margin:0 .5rem}.content-wrapper.kanban .card.card-row:first-child{margin-left:0}.content-wrapper.kanban .card.card-row .card-body{height:calc(100% - (12px + (1.8rem * 1.2) + .5rem));overflow-y:auto}.content-wrapper.kanban .card.card-row .card:last-child{margin-bottom:0;border-bottom-width:1px}.content-wrapper.kanban .card.card-row .card .card-header{padding:.5rem .75rem}.content-wrapper.kanban .card.card-row .card .card-body{padding:.75rem}.content-wrapper.kanban .btn-tool.btn-link{text-decoration:underline;padding-left:0;padding-right:0}.fc-button{background:#f8f9fa;background-image:none;border-bottom-color:#ddd;border-color:#ddd;color:#495057}.fc-button.hover,.fc-button:active,.fc-button:hover{background-color:#e9e9e9}.fc-header-title h2{color:#666;font-size:15px;line-height:1.6em;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{border:0;width:100%}.fc-widget-content:first-of-type,.fc-widget-header:first-of-type{border-left:0;border-right:0}.fc-widget-content:last-of-type,.fc-widget-header:last-of-type{border-right:0}.fc-toolbar,.fc-toolbar.fc-header-toolbar{margin:0;padding:1rem}@media (max-width:575.98px){.fc-toolbar{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.fc-toolbar .fc-left{-webkit-order:1;-ms-flex-order:1;order:1;margin-bottom:.5rem}.fc-toolbar .fc-center{-webkit-order:0;-ms-flex-order:0;order:0;margin-bottom:.375rem}.fc-toolbar .fc-right{-webkit-order:2;-ms-flex-order:2;order:2}}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;line-height:30px;margin-right:5px}.fc-color-picker>li .fa,.fc-color-picker>li .fab,.fc-color-picker>li .fad,.fc-color-picker>li .fal,.fc-color-picker>li .far,.fc-color-picker>li .fas,.fc-color-picker>li .ion,.fc-color-picker>li .svg-inline--fa{transition:-webkit-transform linear .3s;transition:transform linear .3s;transition:transform linear .3s,-webkit-transform linear .3s}.fc-color-picker>li .fa:hover,.fc-color-picker>li .fab:hover,.fc-color-picker>li .fad:hover,.fc-color-picker>li .fal:hover,.fc-color-picker>li .far:hover,.fc-color-picker>li .fas:hover,.fc-color-picker>li .ion:hover,.fc-color-picker>li .svg-inline--fa:hover{-webkit-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{transition:all linear .3s}.external-event{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);border-radius:.25rem;cursor:move;font-weight:700;margin-bottom:4px;padding:5px 10px}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,.2)}.select2-container--default .select2-selection--single{border:1px solid #ced4da;padding:.46875rem .75rem;height:calc(2.25rem + 2px)}.select2-container--default.select2-container--open .select2-selection--single{border-color:#80bdff}.select2-container--default .select2-dropdown{border:1px solid #ced4da}.select2-container--default .select2-results__option{padding:6px 12px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select2-container--default .select2-selection--single .select2-selection__rendered{padding-left:0;height:auto;margin-top:-3px}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:31px;right:6px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-container--default .select2-dropdown .select2-search__field,.select2-container--default .select2-search--inline .select2-search__field{border:1px solid #ced4da}.select2-container--default .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-search--inline .select2-search__field:focus{outline:0;border:1px solid #80bdff}.select2-container--default .select2-dropdown.select2-dropdown--below{border-top:0}.select2-container--default .select2-dropdown.select2-dropdown--above{border-bottom:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#6c757d}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#dee2e6}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#1f2d3d}.select2-container--default .select2-results__option--highlighted{background-color:#007bff;color:#fff}.select2-container--default .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#0074f0;color:#fff}.select2-container--default .select2-selection--multiple{border:1px solid #ced4da;min-height:calc(2.25rem + 2px)}.select2-container--default .select2-selection--multiple:focus{border-color:#80bdff}.select2-container--default .select2-selection--multiple .select2-selection__rendered{padding:0 .375rem .375rem;margin-bottom:-.375rem}.select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline{width:100%;margin-left:.375rem}.select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline .select2-search__field{width:100%!important}.select2-container--default .select2-selection--multiple .select2-selection__rendered .select2-search.select2-search--inline .select2-search__field{border:0;margin-top:6px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#007bff;border-color:#006fe6;color:#fff;padding:0 10px;margin-top:.31rem}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7);float:right;margin-left:5px;margin-right:-2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-selection--multiple.text-sm .select2-search.select2-search--inline .select2-search__field,.text-sm .select2-container--default .select2-selection--multiple .select2-search.select2-search--inline .select2-search__field{margin-top:8px}.select2-container--default .select2-selection--multiple.text-sm .select2-selection__choice,.text-sm .select2-container--default .select2-selection--multiple .select2-selection__choice{margin-top:.4rem}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default.select2-container--focus .select2-selection--single{border-color:#80bdff}.select2-container--default.select2-container--focus .select2-search__field{border:0}.select2-container--default .select2-selection--single .select2-selection__rendered li{padding-right:10px}.input-group-prepend~.select2-container--default .select2-selection{border-bottom-left-radius:0;border-top-left-radius:0}.input-group>.select2-container--default:not(:last-child) .select2-selection{border-bottom-right-radius:0;border-top-right-radius:0}.select2-container--bootstrap4.select2-container--focus .select2-selection{box-shadow:none}select.form-control-sm~.select2-container--default{font-size:.875rem}.text-sm .select2-container--default .select2-selection--single,select.form-control-sm~.select2-container--default .select2-selection--single{height:calc(1.8125rem + 2px)}.text-sm .select2-container--default .select2-selection--single .select2-selection__rendered,select.form-control-sm~.select2-container--default .select2-selection--single .select2-selection__rendered{margin-top:-.4rem}.text-sm .select2-container--default .select2-selection--single .select2-selection__arrow,select.form-control-sm~.select2-container--default .select2-selection--single .select2-selection__arrow{top:-.12rem}.text-sm .select2-container--default .select2-selection--multiple,select.form-control-sm~.select2-container--default .select2-selection--multiple{min-height:calc(1.8125rem + 2px)}.text-sm .select2-container--default .select2-selection--multiple .select2-selection__rendered,select.form-control-sm~.select2-container--default .select2-selection--multiple .select2-selection__rendered{padding:0 .25rem .25rem;margin-top:-.1rem}.text-sm .select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline,select.form-control-sm~.select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline{margin-left:.25rem}.text-sm .select2-container--default .select2-selection--multiple .select2-selection__rendered .select2-search.select2-search--inline .select2-search__field,select.form-control-sm~.select2-container--default .select2-selection--multiple .select2-selection__rendered .select2-search.select2-search--inline .select2-search__field{margin-top:6px}.maximized-card .select2-dropdown{z-index:9999}.select2-primary+.select2-container--default.select2-container--open .select2-selection--single{border-color:#80bdff}.select2-primary+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#80bdff}.select2-container--default .select2-primary .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-primary .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-primary.select2-dropdown .select2-search__field:focus,.select2-primary .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-primary .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-primary .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #80bdff}.select2-container--default .select2-primary .select2-results__option--highlighted,.select2-primary .select2-container--default .select2-results__option--highlighted{background-color:#007bff;color:#fff}.select2-container--default .select2-primary .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-primary .select2-results__option--highlighted[aria-selected]:hover,.select2-primary .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-primary .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#0074f0;color:#fff}.select2-container--default .select2-primary .select2-selection--multiple:focus,.select2-primary .select2-container--default .select2-selection--multiple:focus{border-color:#80bdff}.select2-container--default .select2-primary .select2-selection--multiple .select2-selection__choice,.select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#007bff;border-color:#006fe6;color:#fff}.select2-container--default .select2-primary .select2-selection--multiple .select2-selection__choice__remove,.select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-primary .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-primary.select2-container--focus .select2-selection--multiple,.select2-primary .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#80bdff}.select2-secondary+.select2-container--default.select2-container--open .select2-selection--single{border-color:#afb5ba}.select2-secondary+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#afb5ba}.select2-container--default .select2-secondary .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-secondary .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-secondary.select2-dropdown .select2-search__field:focus,.select2-secondary .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-secondary .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-secondary .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #afb5ba}.select2-container--default .select2-secondary .select2-results__option--highlighted,.select2-secondary .select2-container--default .select2-results__option--highlighted{background-color:#6c757d;color:#fff}.select2-container--default .select2-secondary .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-secondary .select2-results__option--highlighted[aria-selected]:hover,.select2-secondary .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-secondary .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#656d75;color:#fff}.select2-container--default .select2-secondary .select2-selection--multiple:focus,.select2-secondary .select2-container--default .select2-selection--multiple:focus{border-color:#afb5ba}.select2-container--default .select2-secondary .select2-selection--multiple .select2-selection__choice,.select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6c757d;border-color:#60686f;color:#fff}.select2-container--default .select2-secondary .select2-selection--multiple .select2-selection__choice__remove,.select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-secondary .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-secondary.select2-container--focus .select2-selection--multiple,.select2-secondary .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#afb5ba}.select2-success+.select2-container--default.select2-container--open .select2-selection--single{border-color:#71dd8a}.select2-success+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#71dd8a}.select2-container--default .select2-success .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-success .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-success.select2-dropdown .select2-search__field:focus,.select2-success .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-success .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-success .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #71dd8a}.select2-container--default .select2-success .select2-results__option--highlighted,.select2-success .select2-container--default .select2-results__option--highlighted{background-color:#28a745;color:#fff}.select2-container--default .select2-success .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-success .select2-results__option--highlighted[aria-selected]:hover,.select2-success .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-success .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#259b40;color:#fff}.select2-container--default .select2-success .select2-selection--multiple:focus,.select2-success .select2-container--default .select2-selection--multiple:focus{border-color:#71dd8a}.select2-container--default .select2-success .select2-selection--multiple .select2-selection__choice,.select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#28a745;border-color:#23923d;color:#fff}.select2-container--default .select2-success .select2-selection--multiple .select2-selection__choice__remove,.select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-success .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-success.select2-container--focus .select2-selection--multiple,.select2-success .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#71dd8a}.select2-info+.select2-container--default.select2-container--open .select2-selection--single{border-color:#63d9ec}.select2-info+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#63d9ec}.select2-container--default .select2-info .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-info .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-info.select2-dropdown .select2-search__field:focus,.select2-info .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-info .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-info .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #63d9ec}.select2-container--default .select2-info .select2-results__option--highlighted,.select2-info .select2-container--default .select2-results__option--highlighted{background-color:#17a2b8;color:#fff}.select2-container--default .select2-info .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-info .select2-results__option--highlighted[aria-selected]:hover,.select2-info .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-info .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#1596aa;color:#fff}.select2-container--default .select2-info .select2-selection--multiple:focus,.select2-info .select2-container--default .select2-selection--multiple:focus{border-color:#63d9ec}.select2-container--default .select2-info .select2-selection--multiple .select2-selection__choice,.select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#17a2b8;border-color:#148ea1;color:#fff}.select2-container--default .select2-info .select2-selection--multiple .select2-selection__choice__remove,.select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-info .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-info.select2-container--focus .select2-selection--multiple,.select2-info .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#63d9ec}.select2-warning+.select2-container--default.select2-container--open .select2-selection--single{border-color:#ffe187}.select2-warning+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#ffe187}.select2-container--default .select2-warning .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-warning .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-warning.select2-dropdown .select2-search__field:focus,.select2-warning .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-warning .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-warning .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #ffe187}.select2-container--default .select2-warning .select2-results__option--highlighted,.select2-warning .select2-container--default .select2-results__option--highlighted{background-color:#ffc107;color:#1f2d3d}.select2-container--default .select2-warning .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-warning .select2-results__option--highlighted[aria-selected]:hover,.select2-warning .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-warning .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#f7b900;color:#1f2d3d}.select2-container--default .select2-warning .select2-selection--multiple:focus,.select2-warning .select2-container--default .select2-selection--multiple:focus{border-color:#ffe187}.select2-container--default .select2-warning .select2-selection--multiple .select2-selection__choice,.select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ffc107;border-color:#edb100;color:#1f2d3d}.select2-container--default .select2-warning .select2-selection--multiple .select2-selection__choice__remove,.select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-warning .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-warning.select2-container--focus .select2-selection--multiple,.select2-warning .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#ffe187}.select2-danger+.select2-container--default.select2-container--open .select2-selection--single{border-color:#efa2a9}.select2-danger+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#efa2a9}.select2-container--default .select2-danger .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-danger .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-danger.select2-dropdown .select2-search__field:focus,.select2-danger .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-danger .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-danger .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #efa2a9}.select2-container--default .select2-danger .select2-results__option--highlighted,.select2-danger .select2-container--default .select2-results__option--highlighted{background-color:#dc3545;color:#fff}.select2-container--default .select2-danger .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-danger .select2-results__option--highlighted[aria-selected]:hover,.select2-danger .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-danger .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#da2839;color:#fff}.select2-container--default .select2-danger .select2-selection--multiple:focus,.select2-danger .select2-container--default .select2-selection--multiple:focus{border-color:#efa2a9}.select2-container--default .select2-danger .select2-selection--multiple .select2-selection__choice,.select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#dc3545;border-color:#d32535;color:#fff}.select2-container--default .select2-danger .select2-selection--multiple .select2-selection__choice__remove,.select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-danger .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-danger.select2-container--focus .select2-selection--multiple,.select2-danger .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#efa2a9}.select2-light+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fff}.select2-light+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fff}.select2-container--default .select2-light .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-light .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-light.select2-dropdown .select2-search__field:focus,.select2-light .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-light .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-light .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #fff}.select2-container--default .select2-light .select2-results__option--highlighted,.select2-light .select2-container--default .select2-results__option--highlighted{background-color:#f8f9fa;color:#1f2d3d}.select2-container--default .select2-light .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-light .select2-results__option--highlighted[aria-selected]:hover,.select2-light .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-light .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#eff1f4;color:#1f2d3d}.select2-container--default .select2-light .select2-selection--multiple:focus,.select2-light .select2-container--default .select2-selection--multiple:focus{border-color:#fff}.select2-container--default .select2-light .select2-selection--multiple .select2-selection__choice,.select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#f8f9fa;border-color:#e9ecef;color:#1f2d3d}.select2-container--default .select2-light .select2-selection--multiple .select2-selection__choice__remove,.select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-light .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-light.select2-container--focus .select2-selection--multiple,.select2-light .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#fff}.select2-dark+.select2-container--default.select2-container--open .select2-selection--single{border-color:#6d7a86}.select2-dark+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#6d7a86}.select2-container--default .select2-dark .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-dark .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-dark.select2-dropdown .select2-search__field:focus,.select2-dark .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-dark .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-dark .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #6d7a86}.select2-container--default .select2-dark .select2-results__option--highlighted,.select2-dark .select2-container--default .select2-results__option--highlighted{background-color:#343a40;color:#fff}.select2-container--default .select2-dark .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-dark .select2-results__option--highlighted[aria-selected]:hover,.select2-dark .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-dark .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#2d3238;color:#fff}.select2-container--default .select2-dark .select2-selection--multiple:focus,.select2-dark .select2-container--default .select2-selection--multiple:focus{border-color:#6d7a86}.select2-container--default .select2-dark .select2-selection--multiple .select2-selection__choice,.select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#343a40;border-color:#292d32;color:#fff}.select2-container--default .select2-dark .select2-selection--multiple .select2-selection__choice__remove,.select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-dark .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-dark.select2-container--focus .select2-selection--multiple,.select2-dark .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#6d7a86}.select2-lightblue+.select2-container--default.select2-container--open .select2-selection--single{border-color:#99c5de}.select2-lightblue+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#99c5de}.select2-container--default .select2-lightblue .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-lightblue .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-lightblue.select2-dropdown .select2-search__field:focus,.select2-lightblue .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-lightblue .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-lightblue .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #99c5de}.select2-container--default .select2-lightblue .select2-results__option--highlighted,.select2-lightblue .select2-container--default .select2-results__option--highlighted{background-color:#3c8dbc;color:#fff}.select2-container--default .select2-lightblue .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-lightblue .select2-results__option--highlighted[aria-selected]:hover,.select2-lightblue .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-lightblue .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#3884b0;color:#fff}.select2-container--default .select2-lightblue .select2-selection--multiple:focus,.select2-lightblue .select2-container--default .select2-selection--multiple:focus{border-color:#99c5de}.select2-container--default .select2-lightblue .select2-selection--multiple .select2-selection__choice,.select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;color:#fff}.select2-container--default .select2-lightblue .select2-selection--multiple .select2-selection__choice__remove,.select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-lightblue .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-lightblue.select2-container--focus .select2-selection--multiple,.select2-lightblue .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#99c5de}.select2-navy+.select2-container--default.select2-container--open .select2-selection--single{border-color:#005ebf}.select2-navy+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#005ebf}.select2-container--default .select2-navy .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-navy .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-navy.select2-dropdown .select2-search__field:focus,.select2-navy .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-navy .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-navy .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #005ebf}.select2-container--default .select2-navy .select2-results__option--highlighted,.select2-navy .select2-container--default .select2-results__option--highlighted{background-color:#001f3f;color:#fff}.select2-container--default .select2-navy .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-navy .select2-results__option--highlighted[aria-selected]:hover,.select2-navy .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-navy .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#001730;color:#fff}.select2-container--default .select2-navy .select2-selection--multiple:focus,.select2-navy .select2-container--default .select2-selection--multiple:focus{border-color:#005ebf}.select2-container--default .select2-navy .select2-selection--multiple .select2-selection__choice,.select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#001f3f;border-color:#001226;color:#fff}.select2-container--default .select2-navy .select2-selection--multiple .select2-selection__choice__remove,.select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-navy .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-navy.select2-container--focus .select2-selection--multiple,.select2-navy .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#005ebf}.select2-olive+.select2-container--default.select2-container--open .select2-selection--single{border-color:#87cfaf}.select2-olive+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#87cfaf}.select2-container--default .select2-olive .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-olive .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-olive.select2-dropdown .select2-search__field:focus,.select2-olive .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-olive .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-olive .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #87cfaf}.select2-container--default .select2-olive .select2-results__option--highlighted,.select2-olive .select2-container--default .select2-results__option--highlighted{background-color:#3d9970;color:#fff}.select2-container--default .select2-olive .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-olive .select2-results__option--highlighted[aria-selected]:hover,.select2-olive .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-olive .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#398e68;color:#fff}.select2-container--default .select2-olive .select2-selection--multiple:focus,.select2-olive .select2-container--default .select2-selection--multiple:focus{border-color:#87cfaf}.select2-container--default .select2-olive .select2-selection--multiple .select2-selection__choice,.select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3d9970;border-color:#368763;color:#fff}.select2-container--default .select2-olive .select2-selection--multiple .select2-selection__choice__remove,.select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-olive .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-olive.select2-container--focus .select2-selection--multiple,.select2-olive .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#87cfaf}.select2-lime+.select2-container--default.select2-container--open .select2-selection--single{border-color:#81ffb8}.select2-lime+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#81ffb8}.select2-container--default .select2-lime .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-lime .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-lime.select2-dropdown .select2-search__field:focus,.select2-lime .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-lime .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-lime .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #81ffb8}.select2-container--default .select2-lime .select2-results__option--highlighted,.select2-lime .select2-container--default .select2-results__option--highlighted{background-color:#01ff70;color:#1f2d3d}.select2-container--default .select2-lime .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-lime .select2-results__option--highlighted[aria-selected]:hover,.select2-lime .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-lime .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#00f169;color:#1f2d3d}.select2-container--default .select2-lime .select2-selection--multiple:focus,.select2-lime .select2-container--default .select2-selection--multiple:focus{border-color:#81ffb8}.select2-container--default .select2-lime .select2-selection--multiple .select2-selection__choice,.select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#01ff70;border-color:#00e765;color:#1f2d3d}.select2-container--default .select2-lime .select2-selection--multiple .select2-selection__choice__remove,.select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-lime .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-lime.select2-container--focus .select2-selection--multiple,.select2-lime .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#81ffb8}.select2-fuchsia+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f88adf}.select2-fuchsia+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f88adf}.select2-container--default .select2-fuchsia .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-fuchsia .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-fuchsia.select2-dropdown .select2-search__field:focus,.select2-fuchsia .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-fuchsia .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-fuchsia .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #f88adf}.select2-container--default .select2-fuchsia .select2-results__option--highlighted,.select2-fuchsia .select2-container--default .select2-results__option--highlighted{background-color:#f012be;color:#fff}.select2-container--default .select2-fuchsia .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-fuchsia .select2-results__option--highlighted[aria-selected]:hover,.select2-fuchsia .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-fuchsia .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#e40eb4;color:#fff}.select2-container--default .select2-fuchsia .select2-selection--multiple:focus,.select2-fuchsia .select2-container--default .select2-selection--multiple:focus{border-color:#f88adf}.select2-container--default .select2-fuchsia .select2-selection--multiple .select2-selection__choice,.select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#f012be;border-color:#db0ead;color:#fff}.select2-container--default .select2-fuchsia .select2-selection--multiple .select2-selection__choice__remove,.select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-fuchsia .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-fuchsia.select2-container--focus .select2-selection--multiple,.select2-fuchsia .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#f88adf}.select2-maroon+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f083ab}.select2-maroon+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f083ab}.select2-container--default .select2-maroon .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-maroon .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-maroon.select2-dropdown .select2-search__field:focus,.select2-maroon .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-maroon .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-maroon .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #f083ab}.select2-container--default .select2-maroon .select2-results__option--highlighted,.select2-maroon .select2-container--default .select2-results__option--highlighted{background-color:#d81b60;color:#fff}.select2-container--default .select2-maroon .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-maroon .select2-results__option--highlighted[aria-selected]:hover,.select2-maroon .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-maroon .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#ca195a;color:#fff}.select2-container--default .select2-maroon .select2-selection--multiple:focus,.select2-maroon .select2-container--default .select2-selection--multiple:focus{border-color:#f083ab}.select2-container--default .select2-maroon .select2-selection--multiple .select2-selection__choice,.select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#d81b60;border-color:#c11856;color:#fff}.select2-container--default .select2-maroon .select2-selection--multiple .select2-selection__choice__remove,.select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-maroon .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-maroon.select2-container--focus .select2-selection--multiple,.select2-maroon .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#f083ab}.select2-blue+.select2-container--default.select2-container--open .select2-selection--single{border-color:#80bdff}.select2-blue+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#80bdff}.select2-blue .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-blue .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-blue .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .select2-blue .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-blue .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-blue.select2-dropdown .select2-search__field:focus{border:1px solid #80bdff}.select2-blue .select2-container--default .select2-results__option--highlighted,.select2-container--default .select2-blue .select2-results__option--highlighted{background-color:#007bff;color:#fff}.select2-blue .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-blue .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .select2-blue .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-blue .select2-results__option--highlighted[aria-selected]:hover{background-color:#0074f0;color:#fff}.select2-blue .select2-container--default .select2-selection--multiple:focus,.select2-container--default .select2-blue .select2-selection--multiple:focus{border-color:#80bdff}.select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .select2-blue .select2-selection--multiple .select2-selection__choice{background-color:#007bff;border-color:#006fe6;color:#fff}.select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .select2-blue .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .select2-blue .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-blue .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-blue.select2-container--focus .select2-selection--multiple{border-color:#80bdff}.select2-indigo+.select2-container--default.select2-container--open .select2-selection--single{border-color:#b389f9}.select2-indigo+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#b389f9}.select2-container--default .select2-indigo .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-indigo .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-indigo.select2-dropdown .select2-search__field:focus,.select2-indigo .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-indigo .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-indigo .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #b389f9}.select2-container--default .select2-indigo .select2-results__option--highlighted,.select2-indigo .select2-container--default .select2-results__option--highlighted{background-color:#6610f2;color:#fff}.select2-container--default .select2-indigo .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-indigo .select2-results__option--highlighted[aria-selected]:hover,.select2-indigo .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-indigo .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#5f0de6;color:#fff}.select2-container--default .select2-indigo .select2-selection--multiple:focus,.select2-indigo .select2-container--default .select2-selection--multiple:focus{border-color:#b389f9}.select2-container--default .select2-indigo .select2-selection--multiple .select2-selection__choice,.select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6610f2;border-color:#5b0cdd;color:#fff}.select2-container--default .select2-indigo .select2-selection--multiple .select2-selection__choice__remove,.select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-indigo .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-indigo.select2-container--focus .select2-selection--multiple,.select2-indigo .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#b389f9}.select2-purple+.select2-container--default.select2-container--open .select2-selection--single{border-color:#b8a2e0}.select2-purple+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#b8a2e0}.select2-container--default .select2-purple .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-purple .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-purple.select2-dropdown .select2-search__field:focus,.select2-purple .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-purple .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-purple .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #b8a2e0}.select2-container--default .select2-purple .select2-results__option--highlighted,.select2-purple .select2-container--default .select2-results__option--highlighted{background-color:#6f42c1;color:#fff}.select2-container--default .select2-purple .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-purple .select2-results__option--highlighted[aria-selected]:hover,.select2-purple .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-purple .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#683cb8;color:#fff}.select2-container--default .select2-purple .select2-selection--multiple:focus,.select2-purple .select2-container--default .select2-selection--multiple:focus{border-color:#b8a2e0}.select2-container--default .select2-purple .select2-selection--multiple .select2-selection__choice,.select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6f42c1;border-color:#643ab0;color:#fff}.select2-container--default .select2-purple .select2-selection--multiple .select2-selection__choice__remove,.select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-purple .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-purple.select2-container--focus .select2-selection--multiple,.select2-purple .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#b8a2e0}.select2-pink+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f6b0d0}.select2-pink+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f6b0d0}.select2-container--default .select2-pink .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-pink .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-pink.select2-dropdown .select2-search__field:focus,.select2-pink .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-pink .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-pink .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #f6b0d0}.select2-container--default .select2-pink .select2-results__option--highlighted,.select2-pink .select2-container--default .select2-results__option--highlighted{background-color:#e83e8c;color:#fff}.select2-container--default .select2-pink .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-pink .select2-results__option--highlighted[aria-selected]:hover,.select2-pink .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-pink .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#e63084;color:#fff}.select2-container--default .select2-pink .select2-selection--multiple:focus,.select2-pink .select2-container--default .select2-selection--multiple:focus{border-color:#f6b0d0}.select2-container--default .select2-pink .select2-selection--multiple .select2-selection__choice,.select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e83e8c;border-color:#e5277e;color:#fff}.select2-container--default .select2-pink .select2-selection--multiple .select2-selection__choice__remove,.select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-pink .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-pink.select2-container--focus .select2-selection--multiple,.select2-pink .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#f6b0d0}.select2-red+.select2-container--default.select2-container--open .select2-selection--single{border-color:#efa2a9}.select2-red+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#efa2a9}.select2-container--default .select2-red .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-red .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-red.select2-dropdown .select2-search__field:focus,.select2-red .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-red .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-red .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #efa2a9}.select2-container--default .select2-red .select2-results__option--highlighted,.select2-red .select2-container--default .select2-results__option--highlighted{background-color:#dc3545;color:#fff}.select2-container--default .select2-red .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-red .select2-results__option--highlighted[aria-selected]:hover,.select2-red .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-red .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#da2839;color:#fff}.select2-container--default .select2-red .select2-selection--multiple:focus,.select2-red .select2-container--default .select2-selection--multiple:focus{border-color:#efa2a9}.select2-container--default .select2-red .select2-selection--multiple .select2-selection__choice,.select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#dc3545;border-color:#d32535;color:#fff}.select2-container--default .select2-red .select2-selection--multiple .select2-selection__choice__remove,.select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-red .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-red.select2-container--focus .select2-selection--multiple,.select2-red .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#efa2a9}.select2-orange+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fec392}.select2-orange+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fec392}.select2-container--default .select2-orange .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-orange .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-orange.select2-dropdown .select2-search__field:focus,.select2-orange .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-orange .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-orange .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #fec392}.select2-container--default .select2-orange .select2-results__option--highlighted,.select2-orange .select2-container--default .select2-results__option--highlighted{background-color:#fd7e14;color:#1f2d3d}.select2-container--default .select2-orange .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-orange .select2-results__option--highlighted[aria-selected]:hover,.select2-orange .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-orange .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#fd7605;color:#fff}.select2-container--default .select2-orange .select2-selection--multiple:focus,.select2-orange .select2-container--default .select2-selection--multiple:focus{border-color:#fec392}.select2-container--default .select2-orange .select2-selection--multiple .select2-selection__choice,.select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#fd7e14;border-color:#f57102;color:#1f2d3d}.select2-container--default .select2-orange .select2-selection--multiple .select2-selection__choice__remove,.select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-orange .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-orange.select2-container--focus .select2-selection--multiple,.select2-orange .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#fec392}.select2-yellow+.select2-container--default.select2-container--open .select2-selection--single{border-color:#ffe187}.select2-yellow+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#ffe187}.select2-container--default .select2-yellow .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-yellow .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-yellow.select2-dropdown .select2-search__field:focus,.select2-yellow .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-yellow .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-yellow .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #ffe187}.select2-container--default .select2-yellow .select2-results__option--highlighted,.select2-yellow .select2-container--default .select2-results__option--highlighted{background-color:#ffc107;color:#1f2d3d}.select2-container--default .select2-yellow .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-yellow .select2-results__option--highlighted[aria-selected]:hover,.select2-yellow .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-yellow .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#f7b900;color:#1f2d3d}.select2-container--default .select2-yellow .select2-selection--multiple:focus,.select2-yellow .select2-container--default .select2-selection--multiple:focus{border-color:#ffe187}.select2-container--default .select2-yellow .select2-selection--multiple .select2-selection__choice,.select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ffc107;border-color:#edb100;color:#1f2d3d}.select2-container--default .select2-yellow .select2-selection--multiple .select2-selection__choice__remove,.select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-yellow .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-yellow.select2-container--focus .select2-selection--multiple,.select2-yellow .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#ffe187}.select2-green+.select2-container--default.select2-container--open .select2-selection--single{border-color:#71dd8a}.select2-green+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#71dd8a}.select2-container--default .select2-green .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-green .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-green.select2-dropdown .select2-search__field:focus,.select2-green .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-green .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-green .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #71dd8a}.select2-container--default .select2-green .select2-results__option--highlighted,.select2-green .select2-container--default .select2-results__option--highlighted{background-color:#28a745;color:#fff}.select2-container--default .select2-green .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-green .select2-results__option--highlighted[aria-selected]:hover,.select2-green .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-green .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#259b40;color:#fff}.select2-container--default .select2-green .select2-selection--multiple:focus,.select2-green .select2-container--default .select2-selection--multiple:focus{border-color:#71dd8a}.select2-container--default .select2-green .select2-selection--multiple .select2-selection__choice,.select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#28a745;border-color:#23923d;color:#fff}.select2-container--default .select2-green .select2-selection--multiple .select2-selection__choice__remove,.select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-green .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-green.select2-container--focus .select2-selection--multiple,.select2-green .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#71dd8a}.select2-teal+.select2-container--default.select2-container--open .select2-selection--single{border-color:#7eeaca}.select2-teal+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#7eeaca}.select2-container--default .select2-teal .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-teal .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-teal.select2-dropdown .select2-search__field:focus,.select2-teal .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-teal .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-teal .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #7eeaca}.select2-container--default .select2-teal .select2-results__option--highlighted,.select2-teal .select2-container--default .select2-results__option--highlighted{background-color:#20c997;color:#fff}.select2-container--default .select2-teal .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-teal .select2-results__option--highlighted[aria-selected]:hover,.select2-teal .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-teal .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#1ebc8d;color:#fff}.select2-container--default .select2-teal .select2-selection--multiple:focus,.select2-teal .select2-container--default .select2-selection--multiple:focus{border-color:#7eeaca}.select2-container--default .select2-teal .select2-selection--multiple .select2-selection__choice,.select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#20c997;border-color:#1cb386;color:#fff}.select2-container--default .select2-teal .select2-selection--multiple .select2-selection__choice__remove,.select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-teal .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-teal.select2-container--focus .select2-selection--multiple,.select2-teal .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#7eeaca}.select2-cyan+.select2-container--default.select2-container--open .select2-selection--single{border-color:#63d9ec}.select2-cyan+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#63d9ec}.select2-container--default .select2-cyan .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-cyan .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-cyan.select2-dropdown .select2-search__field:focus,.select2-cyan .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-cyan .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-cyan .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #63d9ec}.select2-container--default .select2-cyan .select2-results__option--highlighted,.select2-cyan .select2-container--default .select2-results__option--highlighted{background-color:#17a2b8;color:#fff}.select2-container--default .select2-cyan .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-cyan .select2-results__option--highlighted[aria-selected]:hover,.select2-cyan .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-cyan .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#1596aa;color:#fff}.select2-container--default .select2-cyan .select2-selection--multiple:focus,.select2-cyan .select2-container--default .select2-selection--multiple:focus{border-color:#63d9ec}.select2-container--default .select2-cyan .select2-selection--multiple .select2-selection__choice,.select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#17a2b8;border-color:#148ea1;color:#fff}.select2-container--default .select2-cyan .select2-selection--multiple .select2-selection__choice__remove,.select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-cyan .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-cyan.select2-container--focus .select2-selection--multiple,.select2-cyan .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#63d9ec}.select2-white+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fff}.select2-white+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fff}.select2-container--default .select2-white .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-white .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-white.select2-dropdown .select2-search__field:focus,.select2-white .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-white .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-white .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #fff}.select2-container--default .select2-white .select2-results__option--highlighted,.select2-white .select2-container--default .select2-results__option--highlighted{background-color:#fff;color:#1f2d3d}.select2-container--default .select2-white .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-white .select2-results__option--highlighted[aria-selected]:hover,.select2-white .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-white .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#f7f7f7;color:#1f2d3d}.select2-container--default .select2-white .select2-selection--multiple:focus,.select2-white .select2-container--default .select2-selection--multiple:focus{border-color:#fff}.select2-container--default .select2-white .select2-selection--multiple .select2-selection__choice,.select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#fff;border-color:#f2f2f2;color:#1f2d3d}.select2-container--default .select2-white .select2-selection--multiple .select2-selection__choice__remove,.select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-white .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-white.select2-container--focus .select2-selection--multiple,.select2-white .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#fff}.select2-gray+.select2-container--default.select2-container--open .select2-selection--single{border-color:#afb5ba}.select2-gray+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#afb5ba}.select2-container--default .select2-gray .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-gray .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-gray.select2-dropdown .select2-search__field:focus,.select2-gray .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-gray .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-gray .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #afb5ba}.select2-container--default .select2-gray .select2-results__option--highlighted,.select2-gray .select2-container--default .select2-results__option--highlighted{background-color:#6c757d;color:#fff}.select2-container--default .select2-gray .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-gray .select2-results__option--highlighted[aria-selected]:hover,.select2-gray .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-gray .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#656d75;color:#fff}.select2-container--default .select2-gray .select2-selection--multiple:focus,.select2-gray .select2-container--default .select2-selection--multiple:focus{border-color:#afb5ba}.select2-container--default .select2-gray .select2-selection--multiple .select2-selection__choice,.select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6c757d;border-color:#60686f;color:#fff}.select2-container--default .select2-gray .select2-selection--multiple .select2-selection__choice__remove,.select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-gray .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-gray.select2-container--focus .select2-selection--multiple,.select2-gray .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#afb5ba}.select2-gray-dark+.select2-container--default.select2-container--open .select2-selection--single{border-color:#6d7a86}.select2-gray-dark+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#6d7a86}.select2-container--default .select2-gray-dark .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-gray-dark .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-gray-dark.select2-dropdown .select2-search__field:focus,.select2-gray-dark .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-gray-dark .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-gray-dark .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #6d7a86}.select2-container--default .select2-gray-dark .select2-results__option--highlighted,.select2-gray-dark .select2-container--default .select2-results__option--highlighted{background-color:#343a40;color:#fff}.select2-container--default .select2-gray-dark .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-gray-dark .select2-results__option--highlighted[aria-selected]:hover,.select2-gray-dark .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-gray-dark .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#2d3238;color:#fff}.select2-container--default .select2-gray-dark .select2-selection--multiple:focus,.select2-gray-dark .select2-container--default .select2-selection--multiple:focus{border-color:#6d7a86}.select2-container--default .select2-gray-dark .select2-selection--multiple .select2-selection__choice,.select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#343a40;border-color:#292d32;color:#fff}.select2-container--default .select2-gray-dark .select2-selection--multiple .select2-selection__choice__remove,.select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-gray-dark .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-gray-dark.select2-container--focus .select2-selection--multiple,.select2-gray-dark .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#6d7a86}.dark-mode .select2-selection{background-color:#343a40;border-color:#6c757d}.dark-mode .select2-container--disabled .select2-selection--single{background-color:#454d55}.dark-mode .select2-selection--single{background-color:#343a40;border-color:#6c757d}.dark-mode .select2-selection--single .select2-selection__rendered{color:#fff}.dark-mode .select2-dropdown .select2-search__field,.dark-mode .select2-search--inline .select2-search__field{background-color:#343a40;border-color:#6c757d;color:#fff}.dark-mode .select2-dropdown{background-color:#343a40;border-color:#6c757d;color:#fff}.dark-mode .select2-results__option[aria-selected=true]{background-color:#3f474e!important;color:#dee2e6}.dark-mode .select2-container .select2-search--inline .select2-search__field{background-color:transparent;color:#fff}.dark-mode .select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice{color:#fff}.dark-mode .select2-primary+.select2-container--default.select2-container--open .select2-selection--single{border-color:#85a7ca}.dark-mode .select2-primary+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#85a7ca}.dark-mode .select2-primary .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-primary .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-primary .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-primary .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-primary .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-primary.select2-dropdown .select2-search__field:focus{border:1px solid #85a7ca}.dark-mode .select2-primary .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-primary .select2-results__option--highlighted{background-color:#3f6791;color:#fff}.dark-mode .select2-primary .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-primary .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-primary .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-primary .select2-results__option--highlighted[aria-selected]:hover{background-color:#3a5f86;color:#fff}.dark-mode .select2-primary .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-primary .select2-selection--multiple:focus{border-color:#85a7ca}.dark-mode .select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-primary .select2-selection--multiple .select2-selection__choice{background-color:#3f6791;border-color:#375a7f;color:#fff}.dark-mode .select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-primary .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-primary .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-primary .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-primary.select2-container--focus .select2-selection--multiple{border-color:#85a7ca}.dark-mode .select2-secondary+.select2-container--default.select2-container--open .select2-selection--single{border-color:#afb5ba}.dark-mode .select2-secondary+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#afb5ba}.dark-mode .select2-secondary .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-secondary .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-secondary .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-secondary .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-secondary .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-secondary.select2-dropdown .select2-search__field:focus{border:1px solid #afb5ba}.dark-mode .select2-secondary .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-secondary .select2-results__option--highlighted{background-color:#6c757d;color:#fff}.dark-mode .select2-secondary .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-secondary .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-secondary .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-secondary .select2-results__option--highlighted[aria-selected]:hover{background-color:#656d75;color:#fff}.dark-mode .select2-secondary .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-secondary .select2-selection--multiple:focus{border-color:#afb5ba}.dark-mode .select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-secondary .select2-selection--multiple .select2-selection__choice{background-color:#6c757d;border-color:#60686f;color:#fff}.dark-mode .select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-secondary .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-secondary .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-secondary .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-secondary.select2-container--focus .select2-selection--multiple{border-color:#afb5ba}.dark-mode .select2-success+.select2-container--default.select2-container--open .select2-selection--single{border-color:#3dffcd}.dark-mode .select2-success+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#3dffcd}.dark-mode .select2-success .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-success .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-success .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-success .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-success .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-success.select2-dropdown .select2-search__field:focus{border:1px solid #3dffcd}.dark-mode .select2-success .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-success .select2-results__option--highlighted{background-color:#00bc8c;color:#fff}.dark-mode .select2-success .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-success .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-success .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-success .select2-results__option--highlighted[aria-selected]:hover{background-color:#00ad81;color:#fff}.dark-mode .select2-success .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-success .select2-selection--multiple:focus{border-color:#3dffcd}.dark-mode .select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-success .select2-selection--multiple .select2-selection__choice{background-color:#00bc8c;border-color:#00a379;color:#fff}.dark-mode .select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-success .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-success .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-success .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-success.select2-container--focus .select2-selection--multiple{border-color:#3dffcd}.dark-mode .select2-info+.select2-container--default.select2-container--open .select2-selection--single{border-color:#a0cfee}.dark-mode .select2-info+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#a0cfee}.dark-mode .select2-info .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-info .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-info .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-info .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-info .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-info.select2-dropdown .select2-search__field:focus{border:1px solid #a0cfee}.dark-mode .select2-info .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-info .select2-results__option--highlighted{background-color:#3498db;color:#fff}.dark-mode .select2-info .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-info .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-info .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-info .select2-results__option--highlighted[aria-selected]:hover{background-color:#2791d9;color:#fff}.dark-mode .select2-info .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-info .select2-selection--multiple:focus{border-color:#a0cfee}.dark-mode .select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-info .select2-selection--multiple .select2-selection__choice{background-color:#3498db;border-color:#258cd1;color:#fff}.dark-mode .select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-info .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-info .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-info .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-info.select2-container--focus .select2-selection--multiple{border-color:#a0cfee}.dark-mode .select2-warning+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f9cf8b}.dark-mode .select2-warning+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f9cf8b}.dark-mode .select2-warning .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-warning .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-warning .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-warning .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-warning .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-warning.select2-dropdown .select2-search__field:focus{border:1px solid #f9cf8b}.dark-mode .select2-warning .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-warning .select2-results__option--highlighted{background-color:#f39c12;color:#1f2d3d}.dark-mode .select2-warning .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-warning .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-warning .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-warning .select2-results__option--highlighted[aria-selected]:hover{background-color:#ea940c;color:#1f2d3d}.dark-mode .select2-warning .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-warning .select2-selection--multiple:focus{border-color:#f9cf8b}.dark-mode .select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-warning .select2-selection--multiple .select2-selection__choice{background-color:#f39c12;border-color:#e08e0b;color:#1f2d3d}.dark-mode .select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-warning .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-warning .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-warning .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-warning.select2-container--focus .select2-selection--multiple{border-color:#f9cf8b}.dark-mode .select2-danger+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f5b4ae}.dark-mode .select2-danger+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f5b4ae}.dark-mode .select2-danger .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-danger .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-danger .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-danger .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-danger .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-danger.select2-dropdown .select2-search__field:focus{border:1px solid #f5b4ae}.dark-mode .select2-danger .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-danger .select2-results__option--highlighted{background-color:#e74c3c;color:#fff}.dark-mode .select2-danger .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-danger .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-danger .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-danger .select2-results__option--highlighted[aria-selected]:hover{background-color:#e53f2e;color:#fff}.dark-mode .select2-danger .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-danger .select2-selection--multiple:focus{border-color:#f5b4ae}.dark-mode .select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-danger .select2-selection--multiple .select2-selection__choice{background-color:#e74c3c;border-color:#e43725;color:#fff}.dark-mode .select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-danger .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-danger .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-danger .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-danger.select2-container--focus .select2-selection--multiple{border-color:#f5b4ae}.dark-mode .select2-light+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fff}.dark-mode .select2-light+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fff}.dark-mode .select2-light .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-light .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-light .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-light .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-light .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-light.select2-dropdown .select2-search__field:focus{border:1px solid #fff}.dark-mode .select2-light .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-light .select2-results__option--highlighted{background-color:#f8f9fa;color:#1f2d3d}.dark-mode .select2-light .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-light .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-light .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-light .select2-results__option--highlighted[aria-selected]:hover{background-color:#eff1f4;color:#1f2d3d}.dark-mode .select2-light .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-light .select2-selection--multiple:focus{border-color:#fff}.dark-mode .select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-light .select2-selection--multiple .select2-selection__choice{background-color:#f8f9fa;border-color:#e9ecef;color:#1f2d3d}.dark-mode .select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-light .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-light .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-light .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-light.select2-container--focus .select2-selection--multiple{border-color:#fff}.dark-mode .select2-dark+.select2-container--default.select2-container--open .select2-selection--single{border-color:#6d7a86}.dark-mode .select2-dark+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#6d7a86}.dark-mode .select2-dark .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-dark .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-dark .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-dark .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-dark .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-dark.select2-dropdown .select2-search__field:focus{border:1px solid #6d7a86}.dark-mode .select2-dark .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-dark .select2-results__option--highlighted{background-color:#343a40;color:#fff}.dark-mode .select2-dark .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-dark .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-dark .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-dark .select2-results__option--highlighted[aria-selected]:hover{background-color:#2d3238;color:#fff}.dark-mode .select2-dark .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-dark .select2-selection--multiple:focus{border-color:#6d7a86}.dark-mode .select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-dark .select2-selection--multiple .select2-selection__choice{background-color:#343a40;border-color:#292d32;color:#fff}.dark-mode .select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-dark .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-dark .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-dark .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-dark.select2-container--focus .select2-selection--multiple{border-color:#6d7a86}.dark-mode .select2-lightblue+.select2-container--default.select2-container--open .select2-selection--single{border-color:#e6f1f7}.dark-mode .select2-lightblue+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#e6f1f7}.dark-mode .select2-lightblue .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-lightblue .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-lightblue .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-lightblue .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-lightblue .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-lightblue.select2-dropdown .select2-search__field:focus{border:1px solid #e6f1f7}.dark-mode .select2-lightblue .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-lightblue .select2-results__option--highlighted{background-color:#86bad8;color:#1f2d3d}.dark-mode .select2-lightblue .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-lightblue .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-lightblue .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-lightblue .select2-results__option--highlighted[aria-selected]:hover{background-color:#7ab3d5;color:#1f2d3d}.dark-mode .select2-lightblue .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-lightblue .select2-selection--multiple:focus{border-color:#e6f1f7}.dark-mode .select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-lightblue .select2-selection--multiple .select2-selection__choice{background-color:#86bad8;border-color:#72afd2;color:#1f2d3d}.dark-mode .select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-lightblue .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-lightblue .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-lightblue .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-lightblue.select2-container--focus .select2-selection--multiple{border-color:#e6f1f7}.dark-mode .select2-navy+.select2-container--default.select2-container--open .select2-selection--single{border-color:#006ad8}.dark-mode .select2-navy+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#006ad8}.dark-mode .select2-navy .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-navy .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-navy .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-navy .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-navy .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-navy.select2-dropdown .select2-search__field:focus{border:1px solid #006ad8}.dark-mode .select2-navy .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-navy .select2-results__option--highlighted{background-color:#002c59;color:#fff}.dark-mode .select2-navy .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-navy .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-navy .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-navy .select2-results__option--highlighted[aria-selected]:hover{background-color:#002449;color:#fff}.dark-mode .select2-navy .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-navy .select2-selection--multiple:focus{border-color:#006ad8}.dark-mode .select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-navy .select2-selection--multiple .select2-selection__choice{background-color:#002c59;border-color:#001f3f;color:#fff}.dark-mode .select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-navy .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-navy .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-navy .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-navy.select2-container--focus .select2-selection--multiple{border-color:#006ad8}.dark-mode .select2-olive+.select2-container--default.select2-container--open .select2-selection--single{border-color:#cfecdf}.dark-mode .select2-olive+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#cfecdf}.dark-mode .select2-olive .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-olive .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-olive .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-olive .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-olive .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-olive.select2-dropdown .select2-search__field:focus{border:1px solid #cfecdf}.dark-mode .select2-olive .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-olive .select2-results__option--highlighted{background-color:#74c8a3;color:#1f2d3d}.dark-mode .select2-olive .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-olive .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-olive .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-olive .select2-results__option--highlighted[aria-selected]:hover{background-color:#69c39b;color:#1f2d3d}.dark-mode .select2-olive .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-olive .select2-selection--multiple:focus{border-color:#cfecdf}.dark-mode .select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-olive .select2-selection--multiple .select2-selection__choice{background-color:#74c8a3;border-color:#62c096;color:#1f2d3d}.dark-mode .select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-olive .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-olive .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-olive .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-olive.select2-container--focus .select2-selection--multiple{border-color:#cfecdf}.dark-mode .select2-lime+.select2-container--default.select2-container--open .select2-selection--single{border-color:#e7fff1}.dark-mode .select2-lime+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#e7fff1}.dark-mode .select2-lime .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-lime .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-lime .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-lime .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-lime .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-lime.select2-dropdown .select2-search__field:focus{border:1px solid #e7fff1}.dark-mode .select2-lime .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-lime .select2-results__option--highlighted{background-color:#67ffa9;color:#1f2d3d}.dark-mode .select2-lime .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-lime .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-lime .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-lime .select2-results__option--highlighted[aria-selected]:hover{background-color:#58ffa1;color:#1f2d3d}.dark-mode .select2-lime .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-lime .select2-selection--multiple:focus{border-color:#e7fff1}.dark-mode .select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-lime .select2-selection--multiple .select2-selection__choice{background-color:#67ffa9;border-color:#4eff9b;color:#1f2d3d}.dark-mode .select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-lime .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-lime .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-lime .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-lime.select2-container--focus .select2-selection--multiple{border-color:#e7fff1}.dark-mode .select2-fuchsia+.select2-container--default.select2-container--open .select2-selection--single{border-color:#feeaf9}.dark-mode .select2-fuchsia+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#feeaf9}.dark-mode .select2-fuchsia .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-fuchsia .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-fuchsia .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-fuchsia .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-fuchsia .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-fuchsia.select2-dropdown .select2-search__field:focus{border:1px solid #feeaf9}.dark-mode .select2-fuchsia .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-fuchsia .select2-results__option--highlighted{background-color:#f672d8;color:#1f2d3d}.dark-mode .select2-fuchsia .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-fuchsia .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-fuchsia .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-fuchsia .select2-results__option--highlighted[aria-selected]:hover{background-color:#f564d4;color:#1f2d3d}.dark-mode .select2-fuchsia .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-fuchsia .select2-selection--multiple:focus{border-color:#feeaf9}.dark-mode .select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-fuchsia .select2-selection--multiple .select2-selection__choice{background-color:#f672d8;border-color:#f55ad2;color:#1f2d3d}.dark-mode .select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-fuchsia .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-fuchsia .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-fuchsia .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-fuchsia.select2-container--focus .select2-selection--multiple{border-color:#feeaf9}.dark-mode .select2-maroon+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fbdee8}.dark-mode .select2-maroon+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fbdee8}.dark-mode .select2-maroon .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-maroon .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-maroon .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-maroon .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-maroon .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-maroon.select2-dropdown .select2-search__field:focus{border:1px solid #fbdee8}.dark-mode .select2-maroon .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-maroon .select2-results__option--highlighted{background-color:#ed6c9b;color:#1f2d3d}.dark-mode .select2-maroon .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-maroon .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-maroon .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-maroon .select2-results__option--highlighted[aria-selected]:hover{background-color:#eb5f92;color:#fff}.dark-mode .select2-maroon .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-maroon .select2-selection--multiple:focus{border-color:#fbdee8}.dark-mode .select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-maroon .select2-selection--multiple .select2-selection__choice{background-color:#ed6c9b;border-color:#ea568c;color:#1f2d3d}.dark-mode .select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-maroon .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-maroon .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-maroon .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-maroon.select2-container--focus .select2-selection--multiple{border-color:#fbdee8}.dark-mode .select2-blue+.select2-container--default.select2-container--open .select2-selection--single{border-color:#85a7ca}.dark-mode .select2-blue+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#85a7ca}.dark-mode .select2-blue .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-blue .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-blue .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-blue .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-blue .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-blue.select2-dropdown .select2-search__field:focus{border:1px solid #85a7ca}.dark-mode .select2-blue .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-blue .select2-results__option--highlighted{background-color:#3f6791;color:#fff}.dark-mode .select2-blue .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-blue .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-blue .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-blue .select2-results__option--highlighted[aria-selected]:hover{background-color:#3a5f86;color:#fff}.dark-mode .select2-blue .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-blue .select2-selection--multiple:focus{border-color:#85a7ca}.dark-mode .select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-blue .select2-selection--multiple .select2-selection__choice{background-color:#3f6791;border-color:#375a7f;color:#fff}.dark-mode .select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-blue .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-blue .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-blue .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-blue.select2-container--focus .select2-selection--multiple{border-color:#85a7ca}.dark-mode .select2-indigo+.select2-container--default.select2-container--open .select2-selection--single{border-color:#b389f9}.dark-mode .select2-indigo+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#b389f9}.dark-mode .select2-indigo .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-indigo .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-indigo .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-indigo .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-indigo .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-indigo.select2-dropdown .select2-search__field:focus{border:1px solid #b389f9}.dark-mode .select2-indigo .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-indigo .select2-results__option--highlighted{background-color:#6610f2;color:#fff}.dark-mode .select2-indigo .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-indigo .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-indigo .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-indigo .select2-results__option--highlighted[aria-selected]:hover{background-color:#5f0de6;color:#fff}.dark-mode .select2-indigo .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-indigo .select2-selection--multiple:focus{border-color:#b389f9}.dark-mode .select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-indigo .select2-selection--multiple .select2-selection__choice{background-color:#6610f2;border-color:#5b0cdd;color:#fff}.dark-mode .select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-indigo .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-indigo .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-indigo .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-indigo.select2-container--focus .select2-selection--multiple{border-color:#b389f9}.dark-mode .select2-purple+.select2-container--default.select2-container--open .select2-selection--single{border-color:#b8a2e0}.dark-mode .select2-purple+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#b8a2e0}.dark-mode .select2-purple .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-purple .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-purple .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-purple .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-purple .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-purple.select2-dropdown .select2-search__field:focus{border:1px solid #b8a2e0}.dark-mode .select2-purple .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-purple .select2-results__option--highlighted{background-color:#6f42c1;color:#fff}.dark-mode .select2-purple .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-purple .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-purple .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-purple .select2-results__option--highlighted[aria-selected]:hover{background-color:#683cb8;color:#fff}.dark-mode .select2-purple .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-purple .select2-selection--multiple:focus{border-color:#b8a2e0}.dark-mode .select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-purple .select2-selection--multiple .select2-selection__choice{background-color:#6f42c1;border-color:#643ab0;color:#fff}.dark-mode .select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-purple .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-purple .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-purple .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-purple.select2-container--focus .select2-selection--multiple{border-color:#b8a2e0}.dark-mode .select2-pink+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f6b0d0}.dark-mode .select2-pink+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f6b0d0}.dark-mode .select2-pink .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-pink .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-pink .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-pink .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-pink .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-pink.select2-dropdown .select2-search__field:focus{border:1px solid #f6b0d0}.dark-mode .select2-pink .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-pink .select2-results__option--highlighted{background-color:#e83e8c;color:#fff}.dark-mode .select2-pink .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-pink .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-pink .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-pink .select2-results__option--highlighted[aria-selected]:hover{background-color:#e63084;color:#fff}.dark-mode .select2-pink .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-pink .select2-selection--multiple:focus{border-color:#f6b0d0}.dark-mode .select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-pink .select2-selection--multiple .select2-selection__choice{background-color:#e83e8c;border-color:#e5277e;color:#fff}.dark-mode .select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-pink .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-pink .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-pink .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-pink.select2-container--focus .select2-selection--multiple{border-color:#f6b0d0}.dark-mode .select2-red+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f5b4ae}.dark-mode .select2-red+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f5b4ae}.dark-mode .select2-red .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-red .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-red .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-red .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-red .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-red.select2-dropdown .select2-search__field:focus{border:1px solid #f5b4ae}.dark-mode .select2-red .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-red .select2-results__option--highlighted{background-color:#e74c3c;color:#fff}.dark-mode .select2-red .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-red .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-red .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-red .select2-results__option--highlighted[aria-selected]:hover{background-color:#e53f2e;color:#fff}.dark-mode .select2-red .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-red .select2-selection--multiple:focus{border-color:#f5b4ae}.dark-mode .select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-red .select2-selection--multiple .select2-selection__choice{background-color:#e74c3c;border-color:#e43725;color:#fff}.dark-mode .select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-red .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-red .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-red .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-red.select2-container--focus .select2-selection--multiple{border-color:#f5b4ae}.dark-mode .select2-orange+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fec392}.dark-mode .select2-orange+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fec392}.dark-mode .select2-orange .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-orange .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-orange .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-orange .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-orange .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-orange.select2-dropdown .select2-search__field:focus{border:1px solid #fec392}.dark-mode .select2-orange .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-orange .select2-results__option--highlighted{background-color:#fd7e14;color:#1f2d3d}.dark-mode .select2-orange .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-orange .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-orange .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-orange .select2-results__option--highlighted[aria-selected]:hover{background-color:#fd7605;color:#fff}.dark-mode .select2-orange .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-orange .select2-selection--multiple:focus{border-color:#fec392}.dark-mode .select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-orange .select2-selection--multiple .select2-selection__choice{background-color:#fd7e14;border-color:#f57102;color:#1f2d3d}.dark-mode .select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-orange .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-orange .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-orange .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-orange.select2-container--focus .select2-selection--multiple{border-color:#fec392}.dark-mode .select2-yellow+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f9cf8b}.dark-mode .select2-yellow+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f9cf8b}.dark-mode .select2-yellow .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-yellow .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-yellow .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-yellow .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-yellow .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-yellow.select2-dropdown .select2-search__field:focus{border:1px solid #f9cf8b}.dark-mode .select2-yellow .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-yellow .select2-results__option--highlighted{background-color:#f39c12;color:#1f2d3d}.dark-mode .select2-yellow .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-yellow .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-yellow .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-yellow .select2-results__option--highlighted[aria-selected]:hover{background-color:#ea940c;color:#1f2d3d}.dark-mode .select2-yellow .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-yellow .select2-selection--multiple:focus{border-color:#f9cf8b}.dark-mode .select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-yellow .select2-selection--multiple .select2-selection__choice{background-color:#f39c12;border-color:#e08e0b;color:#1f2d3d}.dark-mode .select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-yellow .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-yellow .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-yellow .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-yellow.select2-container--focus .select2-selection--multiple{border-color:#f9cf8b}.dark-mode .select2-green+.select2-container--default.select2-container--open .select2-selection--single{border-color:#3dffcd}.dark-mode .select2-green+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#3dffcd}.dark-mode .select2-green .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-green .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-green .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-green .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-green .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-green.select2-dropdown .select2-search__field:focus{border:1px solid #3dffcd}.dark-mode .select2-green .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-green .select2-results__option--highlighted{background-color:#00bc8c;color:#fff}.dark-mode .select2-green .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-green .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-green .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-green .select2-results__option--highlighted[aria-selected]:hover{background-color:#00ad81;color:#fff}.dark-mode .select2-green .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-green .select2-selection--multiple:focus{border-color:#3dffcd}.dark-mode .select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-green .select2-selection--multiple .select2-selection__choice{background-color:#00bc8c;border-color:#00a379;color:#fff}.dark-mode .select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-green .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-green .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-green .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-green.select2-container--focus .select2-selection--multiple{border-color:#3dffcd}.dark-mode .select2-teal+.select2-container--default.select2-container--open .select2-selection--single{border-color:#7eeaca}.dark-mode .select2-teal+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#7eeaca}.dark-mode .select2-teal .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-teal .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-teal .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-teal .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-teal .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-teal.select2-dropdown .select2-search__field:focus{border:1px solid #7eeaca}.dark-mode .select2-teal .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-teal .select2-results__option--highlighted{background-color:#20c997;color:#fff}.dark-mode .select2-teal .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-teal .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-teal .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-teal .select2-results__option--highlighted[aria-selected]:hover{background-color:#1ebc8d;color:#fff}.dark-mode .select2-teal .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-teal .select2-selection--multiple:focus{border-color:#7eeaca}.dark-mode .select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-teal .select2-selection--multiple .select2-selection__choice{background-color:#20c997;border-color:#1cb386;color:#fff}.dark-mode .select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-teal .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-teal .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-teal .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-teal.select2-container--focus .select2-selection--multiple{border-color:#7eeaca}.dark-mode .select2-cyan+.select2-container--default.select2-container--open .select2-selection--single{border-color:#a0cfee}.dark-mode .select2-cyan+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#a0cfee}.dark-mode .select2-cyan .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-cyan .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-cyan .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-cyan .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-cyan .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-cyan.select2-dropdown .select2-search__field:focus{border:1px solid #a0cfee}.dark-mode .select2-cyan .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-cyan .select2-results__option--highlighted{background-color:#3498db;color:#fff}.dark-mode .select2-cyan .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-cyan .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-cyan .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-cyan .select2-results__option--highlighted[aria-selected]:hover{background-color:#2791d9;color:#fff}.dark-mode .select2-cyan .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-cyan .select2-selection--multiple:focus{border-color:#a0cfee}.dark-mode .select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-cyan .select2-selection--multiple .select2-selection__choice{background-color:#3498db;border-color:#258cd1;color:#fff}.dark-mode .select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-cyan .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-cyan .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-cyan .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-cyan.select2-container--focus .select2-selection--multiple{border-color:#a0cfee}.dark-mode .select2-white+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fff}.dark-mode .select2-white+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fff}.dark-mode .select2-white .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-white .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-white .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-white .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-white .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-white.select2-dropdown .select2-search__field:focus{border:1px solid #fff}.dark-mode .select2-white .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-white .select2-results__option--highlighted{background-color:#fff;color:#1f2d3d}.dark-mode .select2-white .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-white .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-white .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-white .select2-results__option--highlighted[aria-selected]:hover{background-color:#f7f7f7;color:#1f2d3d}.dark-mode .select2-white .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-white .select2-selection--multiple:focus{border-color:#fff}.dark-mode .select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-white .select2-selection--multiple .select2-selection__choice{background-color:#fff;border-color:#f2f2f2;color:#1f2d3d}.dark-mode .select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-white .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.dark-mode .select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-white .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.dark-mode .select2-white .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-white.select2-container--focus .select2-selection--multiple{border-color:#fff}.dark-mode .select2-gray+.select2-container--default.select2-container--open .select2-selection--single{border-color:#afb5ba}.dark-mode .select2-gray+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#afb5ba}.dark-mode .select2-gray .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-gray .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-gray .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-gray .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-gray .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-gray.select2-dropdown .select2-search__field:focus{border:1px solid #afb5ba}.dark-mode .select2-gray .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-gray .select2-results__option--highlighted{background-color:#6c757d;color:#fff}.dark-mode .select2-gray .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-gray .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-gray .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-gray .select2-results__option--highlighted[aria-selected]:hover{background-color:#656d75;color:#fff}.dark-mode .select2-gray .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-gray .select2-selection--multiple:focus{border-color:#afb5ba}.dark-mode .select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-gray .select2-selection--multiple .select2-selection__choice{background-color:#6c757d;border-color:#60686f;color:#fff}.dark-mode .select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-gray .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-gray .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-gray .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-gray.select2-container--focus .select2-selection--multiple{border-color:#afb5ba}.dark-mode .select2-gray-dark+.select2-container--default.select2-container--open .select2-selection--single{border-color:#6d7a86}.dark-mode .select2-gray-dark+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#6d7a86}.dark-mode .select2-gray-dark .select2-container--default .select2-dropdown .select2-search__field:focus,.dark-mode .select2-gray-dark .select2-container--default .select2-search--inline .select2-search__field:focus,.dark-mode .select2-gray-dark .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-gray-dark .select2-dropdown .select2-search__field:focus,.select2-container--default .dark-mode .select2-gray-dark .select2-search--inline .select2-search__field:focus,.select2-container--default .dark-mode .select2-gray-dark.select2-dropdown .select2-search__field:focus{border:1px solid #6d7a86}.dark-mode .select2-gray-dark .select2-container--default .select2-results__option--highlighted,.select2-container--default .dark-mode .select2-gray-dark .select2-results__option--highlighted{background-color:#343a40;color:#fff}.dark-mode .select2-gray-dark .select2-container--default .select2-results__option--highlighted[aria-selected],.dark-mode .select2-gray-dark .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .dark-mode .select2-gray-dark .select2-results__option--highlighted[aria-selected],.select2-container--default .dark-mode .select2-gray-dark .select2-results__option--highlighted[aria-selected]:hover{background-color:#2d3238;color:#fff}.dark-mode .select2-gray-dark .select2-container--default .select2-selection--multiple:focus,.select2-container--default .dark-mode .select2-gray-dark .select2-selection--multiple:focus{border-color:#6d7a86}.dark-mode .select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .dark-mode .select2-gray-dark .select2-selection--multiple .select2-selection__choice{background-color:#343a40;border-color:#292d32;color:#fff}.dark-mode .select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .dark-mode .select2-gray-dark .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.dark-mode .select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .dark-mode .select2-gray-dark .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.dark-mode .select2-gray-dark .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .dark-mode .select2-gray-dark.select2-container--focus .select2-selection--multiple{border-color:#6d7a86}.slider .tooltip.in{opacity:.9}.slider.slider-vertical{height:100%}.slider.slider-horizontal{width:100%}.slider-primary .slider .slider-selection{background:#007bff}.slider-secondary .slider .slider-selection{background:#6c757d}.slider-success .slider .slider-selection{background:#28a745}.slider-info .slider .slider-selection{background:#17a2b8}.slider-warning .slider .slider-selection{background:#ffc107}.slider-danger .slider .slider-selection{background:#dc3545}.slider-light .slider .slider-selection{background:#f8f9fa}.slider-dark .slider .slider-selection{background:#343a40}.slider-lightblue .slider .slider-selection{background:#3c8dbc}.slider-navy .slider .slider-selection{background:#001f3f}.slider-olive .slider .slider-selection{background:#3d9970}.slider-lime .slider .slider-selection{background:#01ff70}.slider-fuchsia .slider .slider-selection{background:#f012be}.slider-maroon .slider .slider-selection{background:#d81b60}.slider-blue .slider .slider-selection{background:#007bff}.slider-indigo .slider .slider-selection{background:#6610f2}.slider-purple .slider .slider-selection{background:#6f42c1}.slider-pink .slider .slider-selection{background:#e83e8c}.slider-red .slider .slider-selection{background:#dc3545}.slider-orange .slider .slider-selection{background:#fd7e14}.slider-yellow .slider .slider-selection{background:#ffc107}.slider-green .slider .slider-selection{background:#28a745}.slider-teal .slider .slider-selection{background:#20c997}.slider-cyan .slider .slider-selection{background:#17a2b8}.slider-white .slider .slider-selection{background:#fff}.slider-gray .slider .slider-selection{background:#6c757d}.slider-gray-dark .slider .slider-selection{background:#343a40}.dark-mode .slider-track{background-color:#4b545c;background-image:none}.dark-mode .slider-primary .slider .slider-selection{background:#3f6791}.dark-mode .slider-secondary .slider .slider-selection{background:#6c757d}.dark-mode .slider-success .slider .slider-selection{background:#00bc8c}.dark-mode .slider-info .slider .slider-selection{background:#3498db}.dark-mode .slider-warning .slider .slider-selection{background:#f39c12}.dark-mode .slider-danger .slider .slider-selection{background:#e74c3c}.dark-mode .slider-light .slider .slider-selection{background:#f8f9fa}.dark-mode .slider-dark .slider .slider-selection{background:#343a40}.dark-mode .slider-lightblue .slider .slider-selection{background:#86bad8}.dark-mode .slider-navy .slider .slider-selection{background:#002c59}.dark-mode .slider-olive .slider .slider-selection{background:#74c8a3}.dark-mode .slider-lime .slider .slider-selection{background:#67ffa9}.dark-mode .slider-fuchsia .slider .slider-selection{background:#f672d8}.dark-mode .slider-maroon .slider .slider-selection{background:#ed6c9b}.dark-mode .slider-blue .slider .slider-selection{background:#3f6791}.dark-mode .slider-indigo .slider .slider-selection{background:#6610f2}.dark-mode .slider-purple .slider .slider-selection{background:#6f42c1}.dark-mode .slider-pink .slider .slider-selection{background:#e83e8c}.dark-mode .slider-red .slider .slider-selection{background:#e74c3c}.dark-mode .slider-orange .slider .slider-selection{background:#fd7e14}.dark-mode .slider-yellow .slider .slider-selection{background:#f39c12}.dark-mode .slider-green .slider .slider-selection{background:#00bc8c}.dark-mode .slider-teal .slider .slider-selection{background:#20c997}.dark-mode .slider-cyan .slider .slider-selection{background:#3498db}.dark-mode .slider-white .slider .slider-selection{background:#fff}.dark-mode .slider-gray .slider .slider-selection{background:#6c757d}.dark-mode .slider-gray-dark .slider .slider-selection{background:#343a40}.icheck-primary>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-primary>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#007bff}.icheck-primary>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-primary>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#007bff}.icheck-primary>input:first-child:checked+input[type=hidden]+label::before,.icheck-primary>input:first-child:checked+label::before{background-color:#007bff;border-color:#007bff}.icheck-secondary>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-secondary>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6c757d}.icheck-secondary>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-secondary>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6c757d}.icheck-secondary>input:first-child:checked+input[type=hidden]+label::before,.icheck-secondary>input:first-child:checked+label::before{background-color:#6c757d;border-color:#6c757d}.icheck-success>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-success>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#28a745}.icheck-success>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-success>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#28a745}.icheck-success>input:first-child:checked+input[type=hidden]+label::before,.icheck-success>input:first-child:checked+label::before{background-color:#28a745;border-color:#28a745}.icheck-info>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-info>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#17a2b8}.icheck-info>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-info>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#17a2b8}.icheck-info>input:first-child:checked+input[type=hidden]+label::before,.icheck-info>input:first-child:checked+label::before{background-color:#17a2b8;border-color:#17a2b8}.icheck-warning>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-warning>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#ffc107}.icheck-warning>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-warning>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#ffc107}.icheck-warning>input:first-child:checked+input[type=hidden]+label::before,.icheck-warning>input:first-child:checked+label::before{background-color:#ffc107;border-color:#ffc107}.icheck-danger>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-danger>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#dc3545}.icheck-danger>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-danger>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#dc3545}.icheck-danger>input:first-child:checked+input[type=hidden]+label::before,.icheck-danger>input:first-child:checked+label::before{background-color:#dc3545;border-color:#dc3545}.icheck-light>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-light>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f8f9fa}.icheck-light>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-light>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f8f9fa}.icheck-light>input:first-child:checked+input[type=hidden]+label::before,.icheck-light>input:first-child:checked+label::before{background-color:#f8f9fa;border-color:#f8f9fa}.icheck-dark>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-dark>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#343a40}.icheck-dark>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-dark>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#343a40}.icheck-dark>input:first-child:checked+input[type=hidden]+label::before,.icheck-dark>input:first-child:checked+label::before{background-color:#343a40;border-color:#343a40}.icheck-lightblue>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-lightblue>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3c8dbc}.icheck-lightblue>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-lightblue>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3c8dbc}.icheck-lightblue>input:first-child:checked+input[type=hidden]+label::before,.icheck-lightblue>input:first-child:checked+label::before{background-color:#3c8dbc;border-color:#3c8dbc}.icheck-navy>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-navy>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#001f3f}.icheck-navy>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-navy>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#001f3f}.icheck-navy>input:first-child:checked+input[type=hidden]+label::before,.icheck-navy>input:first-child:checked+label::before{background-color:#001f3f;border-color:#001f3f}.icheck-olive>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-olive>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3d9970}.icheck-olive>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-olive>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3d9970}.icheck-olive>input:first-child:checked+input[type=hidden]+label::before,.icheck-olive>input:first-child:checked+label::before{background-color:#3d9970;border-color:#3d9970}.icheck-lime>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-lime>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#01ff70}.icheck-lime>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-lime>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#01ff70}.icheck-lime>input:first-child:checked+input[type=hidden]+label::before,.icheck-lime>input:first-child:checked+label::before{background-color:#01ff70;border-color:#01ff70}.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f012be}.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f012be}.icheck-fuchsia>input:first-child:checked+input[type=hidden]+label::before,.icheck-fuchsia>input:first-child:checked+label::before{background-color:#f012be;border-color:#f012be}.icheck-maroon>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-maroon>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#d81b60}.icheck-maroon>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-maroon>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#d81b60}.icheck-maroon>input:first-child:checked+input[type=hidden]+label::before,.icheck-maroon>input:first-child:checked+label::before{background-color:#d81b60;border-color:#d81b60}.icheck-blue>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-blue>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#007bff}.icheck-blue>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-blue>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#007bff}.icheck-blue>input:first-child:checked+input[type=hidden]+label::before,.icheck-blue>input:first-child:checked+label::before{background-color:#007bff;border-color:#007bff}.icheck-indigo>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-indigo>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6610f2}.icheck-indigo>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-indigo>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6610f2}.icheck-indigo>input:first-child:checked+input[type=hidden]+label::before,.icheck-indigo>input:first-child:checked+label::before{background-color:#6610f2;border-color:#6610f2}.icheck-purple>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-purple>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6f42c1}.icheck-purple>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-purple>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6f42c1}.icheck-purple>input:first-child:checked+input[type=hidden]+label::before,.icheck-purple>input:first-child:checked+label::before{background-color:#6f42c1;border-color:#6f42c1}.icheck-pink>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-pink>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#e83e8c}.icheck-pink>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-pink>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#e83e8c}.icheck-pink>input:first-child:checked+input[type=hidden]+label::before,.icheck-pink>input:first-child:checked+label::before{background-color:#e83e8c;border-color:#e83e8c}.icheck-red>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-red>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#dc3545}.icheck-red>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-red>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#dc3545}.icheck-red>input:first-child:checked+input[type=hidden]+label::before,.icheck-red>input:first-child:checked+label::before{background-color:#dc3545;border-color:#dc3545}.icheck-orange>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-orange>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#fd7e14}.icheck-orange>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-orange>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#fd7e14}.icheck-orange>input:first-child:checked+input[type=hidden]+label::before,.icheck-orange>input:first-child:checked+label::before{background-color:#fd7e14;border-color:#fd7e14}.icheck-yellow>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-yellow>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#ffc107}.icheck-yellow>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-yellow>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#ffc107}.icheck-yellow>input:first-child:checked+input[type=hidden]+label::before,.icheck-yellow>input:first-child:checked+label::before{background-color:#ffc107;border-color:#ffc107}.icheck-green>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-green>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#28a745}.icheck-green>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-green>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#28a745}.icheck-green>input:first-child:checked+input[type=hidden]+label::before,.icheck-green>input:first-child:checked+label::before{background-color:#28a745;border-color:#28a745}.icheck-teal>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-teal>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#20c997}.icheck-teal>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-teal>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#20c997}.icheck-teal>input:first-child:checked+input[type=hidden]+label::before,.icheck-teal>input:first-child:checked+label::before{background-color:#20c997;border-color:#20c997}.icheck-cyan>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-cyan>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#17a2b8}.icheck-cyan>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-cyan>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#17a2b8}.icheck-cyan>input:first-child:checked+input[type=hidden]+label::before,.icheck-cyan>input:first-child:checked+label::before{background-color:#17a2b8;border-color:#17a2b8}.icheck-white>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-white>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#fff}.icheck-white>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-white>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#fff}.icheck-white>input:first-child:checked+input[type=hidden]+label::before,.icheck-white>input:first-child:checked+label::before{background-color:#fff;border-color:#fff}.icheck-gray>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-gray>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6c757d}.icheck-gray>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-gray>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6c757d}.icheck-gray>input:first-child:checked+input[type=hidden]+label::before,.icheck-gray>input:first-child:checked+label::before{background-color:#6c757d;border-color:#6c757d}.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#343a40}.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#343a40}.icheck-gray-dark>input:first-child:checked+input[type=hidden]+label::before,.icheck-gray-dark>input:first-child:checked+label::before{background-color:#343a40;border-color:#343a40}.dark-mode [class*=icheck-]>input:first-child:not(:checked)+input[type=hidden]+label::before,.dark-mode [class*=icheck-]>input:first-child:not(:checked)+label::before{border-color:#6c757d}.dark-mode .icheck-primary>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-primary>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3f6791}.dark-mode .icheck-primary>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-primary>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3f6791}.dark-mode .icheck-primary>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-primary>input:first-child:checked+label::before{background-color:#3f6791;border-color:#3f6791}.dark-mode .icheck-secondary>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-secondary>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6c757d}.dark-mode .icheck-secondary>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-secondary>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6c757d}.dark-mode .icheck-secondary>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-secondary>input:first-child:checked+label::before{background-color:#6c757d;border-color:#6c757d}.dark-mode .icheck-success>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-success>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#00bc8c}.dark-mode .icheck-success>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-success>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#00bc8c}.dark-mode .icheck-success>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-success>input:first-child:checked+label::before{background-color:#00bc8c;border-color:#00bc8c}.dark-mode .icheck-info>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-info>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3498db}.dark-mode .icheck-info>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-info>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3498db}.dark-mode .icheck-info>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-info>input:first-child:checked+label::before{background-color:#3498db;border-color:#3498db}.dark-mode .icheck-warning>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-warning>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f39c12}.dark-mode .icheck-warning>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-warning>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f39c12}.dark-mode .icheck-warning>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-warning>input:first-child:checked+label::before{background-color:#f39c12;border-color:#f39c12}.dark-mode .icheck-danger>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-danger>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#e74c3c}.dark-mode .icheck-danger>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-danger>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#e74c3c}.dark-mode .icheck-danger>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-danger>input:first-child:checked+label::before{background-color:#e74c3c;border-color:#e74c3c}.dark-mode .icheck-light>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-light>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f8f9fa}.dark-mode .icheck-light>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-light>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f8f9fa}.dark-mode .icheck-light>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-light>input:first-child:checked+label::before{background-color:#f8f9fa;border-color:#f8f9fa}.dark-mode .icheck-dark>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-dark>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#343a40}.dark-mode .icheck-dark>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-dark>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#343a40}.dark-mode .icheck-dark>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-dark>input:first-child:checked+label::before{background-color:#343a40;border-color:#343a40}.dark-mode .icheck-lightblue>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-lightblue>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#86bad8}.dark-mode .icheck-lightblue>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-lightblue>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#86bad8}.dark-mode .icheck-lightblue>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-lightblue>input:first-child:checked+label::before{background-color:#86bad8;border-color:#86bad8}.dark-mode .icheck-navy>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-navy>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#002c59}.dark-mode .icheck-navy>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-navy>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#002c59}.dark-mode .icheck-navy>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-navy>input:first-child:checked+label::before{background-color:#002c59;border-color:#002c59}.dark-mode .icheck-olive>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-olive>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#74c8a3}.dark-mode .icheck-olive>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-olive>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#74c8a3}.dark-mode .icheck-olive>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-olive>input:first-child:checked+label::before{background-color:#74c8a3;border-color:#74c8a3}.dark-mode .icheck-lime>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-lime>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#67ffa9}.dark-mode .icheck-lime>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-lime>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#67ffa9}.dark-mode .icheck-lime>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-lime>input:first-child:checked+label::before{background-color:#67ffa9;border-color:#67ffa9}.dark-mode .icheck-fuchsia>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-fuchsia>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f672d8}.dark-mode .icheck-fuchsia>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-fuchsia>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f672d8}.dark-mode .icheck-fuchsia>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-fuchsia>input:first-child:checked+label::before{background-color:#f672d8;border-color:#f672d8}.dark-mode .icheck-maroon>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-maroon>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#ed6c9b}.dark-mode .icheck-maroon>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-maroon>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#ed6c9b}.dark-mode .icheck-maroon>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-maroon>input:first-child:checked+label::before{background-color:#ed6c9b;border-color:#ed6c9b}.dark-mode .icheck-blue>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-blue>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3f6791}.dark-mode .icheck-blue>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-blue>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3f6791}.dark-mode .icheck-blue>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-blue>input:first-child:checked+label::before{background-color:#3f6791;border-color:#3f6791}.dark-mode .icheck-indigo>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-indigo>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6610f2}.dark-mode .icheck-indigo>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-indigo>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6610f2}.dark-mode .icheck-indigo>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-indigo>input:first-child:checked+label::before{background-color:#6610f2;border-color:#6610f2}.dark-mode .icheck-purple>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-purple>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6f42c1}.dark-mode .icheck-purple>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-purple>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6f42c1}.dark-mode .icheck-purple>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-purple>input:first-child:checked+label::before{background-color:#6f42c1;border-color:#6f42c1}.dark-mode .icheck-pink>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-pink>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#e83e8c}.dark-mode .icheck-pink>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-pink>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#e83e8c}.dark-mode .icheck-pink>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-pink>input:first-child:checked+label::before{background-color:#e83e8c;border-color:#e83e8c}.dark-mode .icheck-red>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-red>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#e74c3c}.dark-mode .icheck-red>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-red>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#e74c3c}.dark-mode .icheck-red>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-red>input:first-child:checked+label::before{background-color:#e74c3c;border-color:#e74c3c}.dark-mode .icheck-orange>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-orange>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#fd7e14}.dark-mode .icheck-orange>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-orange>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#fd7e14}.dark-mode .icheck-orange>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-orange>input:first-child:checked+label::before{background-color:#fd7e14;border-color:#fd7e14}.dark-mode .icheck-yellow>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-yellow>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f39c12}.dark-mode .icheck-yellow>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-yellow>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f39c12}.dark-mode .icheck-yellow>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-yellow>input:first-child:checked+label::before{background-color:#f39c12;border-color:#f39c12}.dark-mode .icheck-green>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-green>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#00bc8c}.dark-mode .icheck-green>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-green>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#00bc8c}.dark-mode .icheck-green>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-green>input:first-child:checked+label::before{background-color:#00bc8c;border-color:#00bc8c}.dark-mode .icheck-teal>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-teal>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#20c997}.dark-mode .icheck-teal>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-teal>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#20c997}.dark-mode .icheck-teal>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-teal>input:first-child:checked+label::before{background-color:#20c997;border-color:#20c997}.dark-mode .icheck-cyan>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-cyan>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3498db}.dark-mode .icheck-cyan>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-cyan>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3498db}.dark-mode .icheck-cyan>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-cyan>input:first-child:checked+label::before{background-color:#3498db;border-color:#3498db}.dark-mode .icheck-white>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-white>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#fff}.dark-mode .icheck-white>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-white>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#fff}.dark-mode .icheck-white>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-white>input:first-child:checked+label::before{background-color:#fff;border-color:#fff}.dark-mode .icheck-gray>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-gray>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6c757d}.dark-mode .icheck-gray>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-gray>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6c757d}.dark-mode .icheck-gray>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-gray>input:first-child:checked+label::before{background-color:#6c757d;border-color:#6c757d}.dark-mode .icheck-gray-dark>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.dark-mode .icheck-gray-dark>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#343a40}.dark-mode .icheck-gray-dark>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.dark-mode .icheck-gray-dark>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#343a40}.dark-mode .icheck-gray-dark>input:first-child:checked+input[type=hidden]+label::before,.dark-mode .icheck-gray-dark>input:first-child:checked+label::before{background-color:#343a40;border-color:#343a40}.mapael .map{position:relative}.mapael .mapTooltip{font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;border-radius:.25rem;font-size:.875rem;background-color:#000;color:#fff;display:block;max-width:200px;padding:.25rem .5rem;position:absolute;text-align:center;word-wrap:break-word;z-index:1070}.mapael .myLegend{background-color:#f8f9fa;border:1px solid #adb5bd;padding:10px;width:600px}.mapael .zoomButton{background-color:#f8f9fa;border:1px solid #ddd;border-radius:.25rem;color:#444;cursor:pointer;font-weight:700;height:16px;left:10px;line-height:14px;padding-left:1px;position:absolute;text-align:center;top:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:16px}.mapael .zoomButton.hover,.mapael .zoomButton:active,.mapael .zoomButton:hover{background-color:#e9ecef;color:#2b2b2b}.mapael .zoomReset{line-height:12px;top:10px}.mapael .zoomIn{top:30px}.mapael .zoomOut{top:50px}.jqvmap-zoomin,.jqvmap-zoomout{background-color:#f8f9fa;border:1px solid #ddd;border-radius:.25rem;color:#444;height:15px;width:15px}.jqvmap-zoomin.hover,.jqvmap-zoomin:active,.jqvmap-zoomin:hover,.jqvmap-zoomout.hover,.jqvmap-zoomout:active,.jqvmap-zoomout:hover{background-color:#e9ecef;color:#2b2b2b}.swal2-icon.swal2-info{border-color:ligthen(#17a2b8,20%);color:#17a2b8}.swal2-icon.swal2-warning{border-color:ligthen(#ffc107,20%);color:#ffc107}.swal2-icon.swal2-error{border-color:ligthen(#dc3545,20%);color:#dc3545}.swal2-icon.swal2-question{border-color:ligthen(#6c757d,20%);color:#6c757d}.swal2-icon.swal2-success{border-color:ligthen(#28a745,20%);color:#28a745}.swal2-icon.swal2-success .swal2-success-ring{border-color:ligthen(#28a745,20%)}.swal2-icon.swal2-success [class^=swal2-success-line]{background-color:#28a745}.dark-mode .swal2-popup{background-color:#343a40;color:#e9ecef}.dark-mode .swal2-popup .swal2-content,.dark-mode .swal2-popup .swal2-title{color:#e9ecef}#toast-container .toast{background-color:#007bff}#toast-container .toast-success{background-color:#28a745}#toast-container .toast-error{background-color:#dc3545}#toast-container .toast-info{background-color:#17a2b8}#toast-container .toast-warning{background-color:#ffc107}.toast-bottom-full-width .toast,.toast-top-full-width .toast{max-width:inherit}.pace{z-index:1048}.pace .pace-progress{z-index:1049}.pace .pace-activity{z-index:1050}.pace-primary .pace .pace-progress{background:#007bff}.pace-barber-shop-primary .pace{background:#fff}.pace-barber-shop-primary .pace .pace-progress{background:#007bff}.pace-barber-shop-primary .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-primary .pace .pace-progress::after{color:rgba(0,123,255,.2)}.pace-bounce-primary .pace .pace-activity{background:#007bff}.pace-center-atom-primary .pace-progress{height:100px;width:80px}.pace-center-atom-primary .pace-progress::before{background:#007bff;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-primary .pace-activity{border-color:#007bff}.pace-center-atom-primary .pace-activity::after,.pace-center-atom-primary .pace-activity::before{border-color:#007bff}.pace-center-circle-primary .pace .pace-progress{background:rgba(0,123,255,.8);color:#fff}.pace-center-radar-primary .pace .pace-activity{border-color:#007bff transparent transparent}.pace-center-radar-primary .pace .pace-activity::before{border-color:#007bff transparent transparent}.pace-center-simple-primary .pace{background:#fff;border-color:#007bff}.pace-center-simple-primary .pace .pace-progress{background:#007bff}.pace-material-primary .pace{color:#007bff}.pace-corner-indicator-primary .pace .pace-activity{background:#007bff}.pace-corner-indicator-primary .pace .pace-activity::after,.pace-corner-indicator-primary .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-primary .pace .pace-activity::before{border-right-color:rgba(0,123,255,.2);border-left-color:rgba(0,123,255,.2)}.pace-corner-indicator-primary .pace .pace-activity::after{border-top-color:rgba(0,123,255,.2);border-bottom-color:rgba(0,123,255,.2)}.pace-fill-left-primary .pace .pace-progress{background-color:rgba(0,123,255,.2)}.pace-flash-primary .pace .pace-progress{background:#007bff}.pace-flash-primary .pace .pace-progress-inner{box-shadow:0 0 10px #007bff,0 0 5px #007bff}.pace-flash-primary .pace .pace-activity{border-top-color:#007bff;border-left-color:#007bff}.pace-loading-bar-primary .pace .pace-progress{background:#007bff;color:#007bff;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-primary .pace .pace-activity{box-shadow:inset 0 0 0 2px #007bff,inset 0 0 0 7px #fff}.pace-mac-osx-primary .pace .pace-progress{background-color:#007bff;box-shadow:inset -1px 0 #007bff,inset 0 -1px #007bff,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-primary .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-primary .pace-progress{color:#007bff}.pace-secondary .pace .pace-progress{background:#6c757d}.pace-barber-shop-secondary .pace{background:#fff}.pace-barber-shop-secondary .pace .pace-progress{background:#6c757d}.pace-barber-shop-secondary .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-secondary .pace .pace-progress::after{color:rgba(108,117,125,.2)}.pace-bounce-secondary .pace .pace-activity{background:#6c757d}.pace-center-atom-secondary .pace-progress{height:100px;width:80px}.pace-center-atom-secondary .pace-progress::before{background:#6c757d;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-secondary .pace-activity{border-color:#6c757d}.pace-center-atom-secondary .pace-activity::after,.pace-center-atom-secondary .pace-activity::before{border-color:#6c757d}.pace-center-circle-secondary .pace .pace-progress{background:rgba(108,117,125,.8);color:#fff}.pace-center-radar-secondary .pace .pace-activity{border-color:#6c757d transparent transparent}.pace-center-radar-secondary .pace .pace-activity::before{border-color:#6c757d transparent transparent}.pace-center-simple-secondary .pace{background:#fff;border-color:#6c757d}.pace-center-simple-secondary .pace .pace-progress{background:#6c757d}.pace-material-secondary .pace{color:#6c757d}.pace-corner-indicator-secondary .pace .pace-activity{background:#6c757d}.pace-corner-indicator-secondary .pace .pace-activity::after,.pace-corner-indicator-secondary .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-secondary .pace .pace-activity::before{border-right-color:rgba(108,117,125,.2);border-left-color:rgba(108,117,125,.2)}.pace-corner-indicator-secondary .pace .pace-activity::after{border-top-color:rgba(108,117,125,.2);border-bottom-color:rgba(108,117,125,.2)}.pace-fill-left-secondary .pace .pace-progress{background-color:rgba(108,117,125,.2)}.pace-flash-secondary .pace .pace-progress{background:#6c757d}.pace-flash-secondary .pace .pace-progress-inner{box-shadow:0 0 10px #6c757d,0 0 5px #6c757d}.pace-flash-secondary .pace .pace-activity{border-top-color:#6c757d;border-left-color:#6c757d}.pace-loading-bar-secondary .pace .pace-progress{background:#6c757d;color:#6c757d;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-secondary .pace .pace-activity{box-shadow:inset 0 0 0 2px #6c757d,inset 0 0 0 7px #fff}.pace-mac-osx-secondary .pace .pace-progress{background-color:#6c757d;box-shadow:inset -1px 0 #6c757d,inset 0 -1px #6c757d,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-secondary .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-secondary .pace-progress{color:#6c757d}.pace-success .pace .pace-progress{background:#28a745}.pace-barber-shop-success .pace{background:#fff}.pace-barber-shop-success .pace .pace-progress{background:#28a745}.pace-barber-shop-success .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-success .pace .pace-progress::after{color:rgba(40,167,69,.2)}.pace-bounce-success .pace .pace-activity{background:#28a745}.pace-center-atom-success .pace-progress{height:100px;width:80px}.pace-center-atom-success .pace-progress::before{background:#28a745;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-success .pace-activity{border-color:#28a745}.pace-center-atom-success .pace-activity::after,.pace-center-atom-success .pace-activity::before{border-color:#28a745}.pace-center-circle-success .pace .pace-progress{background:rgba(40,167,69,.8);color:#fff}.pace-center-radar-success .pace .pace-activity{border-color:#28a745 transparent transparent}.pace-center-radar-success .pace .pace-activity::before{border-color:#28a745 transparent transparent}.pace-center-simple-success .pace{background:#fff;border-color:#28a745}.pace-center-simple-success .pace .pace-progress{background:#28a745}.pace-material-success .pace{color:#28a745}.pace-corner-indicator-success .pace .pace-activity{background:#28a745}.pace-corner-indicator-success .pace .pace-activity::after,.pace-corner-indicator-success .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-success .pace .pace-activity::before{border-right-color:rgba(40,167,69,.2);border-left-color:rgba(40,167,69,.2)}.pace-corner-indicator-success .pace .pace-activity::after{border-top-color:rgba(40,167,69,.2);border-bottom-color:rgba(40,167,69,.2)}.pace-fill-left-success .pace .pace-progress{background-color:rgba(40,167,69,.2)}.pace-flash-success .pace .pace-progress{background:#28a745}.pace-flash-success .pace .pace-progress-inner{box-shadow:0 0 10px #28a745,0 0 5px #28a745}.pace-flash-success .pace .pace-activity{border-top-color:#28a745;border-left-color:#28a745}.pace-loading-bar-success .pace .pace-progress{background:#28a745;color:#28a745;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-success .pace .pace-activity{box-shadow:inset 0 0 0 2px #28a745,inset 0 0 0 7px #fff}.pace-mac-osx-success .pace .pace-progress{background-color:#28a745;box-shadow:inset -1px 0 #28a745,inset 0 -1px #28a745,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-success .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-success .pace-progress{color:#28a745}.pace-info .pace .pace-progress{background:#17a2b8}.pace-barber-shop-info .pace{background:#fff}.pace-barber-shop-info .pace .pace-progress{background:#17a2b8}.pace-barber-shop-info .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-info .pace .pace-progress::after{color:rgba(23,162,184,.2)}.pace-bounce-info .pace .pace-activity{background:#17a2b8}.pace-center-atom-info .pace-progress{height:100px;width:80px}.pace-center-atom-info .pace-progress::before{background:#17a2b8;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-info .pace-activity{border-color:#17a2b8}.pace-center-atom-info .pace-activity::after,.pace-center-atom-info .pace-activity::before{border-color:#17a2b8}.pace-center-circle-info .pace .pace-progress{background:rgba(23,162,184,.8);color:#fff}.pace-center-radar-info .pace .pace-activity{border-color:#17a2b8 transparent transparent}.pace-center-radar-info .pace .pace-activity::before{border-color:#17a2b8 transparent transparent}.pace-center-simple-info .pace{background:#fff;border-color:#17a2b8}.pace-center-simple-info .pace .pace-progress{background:#17a2b8}.pace-material-info .pace{color:#17a2b8}.pace-corner-indicator-info .pace .pace-activity{background:#17a2b8}.pace-corner-indicator-info .pace .pace-activity::after,.pace-corner-indicator-info .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-info .pace .pace-activity::before{border-right-color:rgba(23,162,184,.2);border-left-color:rgba(23,162,184,.2)}.pace-corner-indicator-info .pace .pace-activity::after{border-top-color:rgba(23,162,184,.2);border-bottom-color:rgba(23,162,184,.2)}.pace-fill-left-info .pace .pace-progress{background-color:rgba(23,162,184,.2)}.pace-flash-info .pace .pace-progress{background:#17a2b8}.pace-flash-info .pace .pace-progress-inner{box-shadow:0 0 10px #17a2b8,0 0 5px #17a2b8}.pace-flash-info .pace .pace-activity{border-top-color:#17a2b8;border-left-color:#17a2b8}.pace-loading-bar-info .pace .pace-progress{background:#17a2b8;color:#17a2b8;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-info .pace .pace-activity{box-shadow:inset 0 0 0 2px #17a2b8,inset 0 0 0 7px #fff}.pace-mac-osx-info .pace .pace-progress{background-color:#17a2b8;box-shadow:inset -1px 0 #17a2b8,inset 0 -1px #17a2b8,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-info .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-info .pace-progress{color:#17a2b8}.pace-warning .pace .pace-progress{background:#ffc107}.pace-barber-shop-warning .pace{background:#1f2d3d}.pace-barber-shop-warning .pace .pace-progress{background:#ffc107}.pace-barber-shop-warning .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-warning .pace .pace-progress::after{color:rgba(255,193,7,.2)}.pace-bounce-warning .pace .pace-activity{background:#ffc107}.pace-center-atom-warning .pace-progress{height:100px;width:80px}.pace-center-atom-warning .pace-progress::before{background:#ffc107;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-warning .pace-activity{border-color:#ffc107}.pace-center-atom-warning .pace-activity::after,.pace-center-atom-warning .pace-activity::before{border-color:#ffc107}.pace-center-circle-warning .pace .pace-progress{background:rgba(255,193,7,.8);color:#1f2d3d}.pace-center-radar-warning .pace .pace-activity{border-color:#ffc107 transparent transparent}.pace-center-radar-warning .pace .pace-activity::before{border-color:#ffc107 transparent transparent}.pace-center-simple-warning .pace{background:#1f2d3d;border-color:#ffc107}.pace-center-simple-warning .pace .pace-progress{background:#ffc107}.pace-material-warning .pace{color:#ffc107}.pace-corner-indicator-warning .pace .pace-activity{background:#ffc107}.pace-corner-indicator-warning .pace .pace-activity::after,.pace-corner-indicator-warning .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-warning .pace .pace-activity::before{border-right-color:rgba(255,193,7,.2);border-left-color:rgba(255,193,7,.2)}.pace-corner-indicator-warning .pace .pace-activity::after{border-top-color:rgba(255,193,7,.2);border-bottom-color:rgba(255,193,7,.2)}.pace-fill-left-warning .pace .pace-progress{background-color:rgba(255,193,7,.2)}.pace-flash-warning .pace .pace-progress{background:#ffc107}.pace-flash-warning .pace .pace-progress-inner{box-shadow:0 0 10px #ffc107,0 0 5px #ffc107}.pace-flash-warning .pace .pace-activity{border-top-color:#ffc107;border-left-color:#ffc107}.pace-loading-bar-warning .pace .pace-progress{background:#ffc107;color:#ffc107;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-warning .pace .pace-activity{box-shadow:inset 0 0 0 2px #ffc107,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-warning .pace .pace-progress{background-color:#ffc107;box-shadow:inset -1px 0 #ffc107,inset 0 -1px #ffc107,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-warning .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-warning .pace-progress{color:#ffc107}.pace-danger .pace .pace-progress{background:#dc3545}.pace-barber-shop-danger .pace{background:#fff}.pace-barber-shop-danger .pace .pace-progress{background:#dc3545}.pace-barber-shop-danger .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-danger .pace .pace-progress::after{color:rgba(220,53,69,.2)}.pace-bounce-danger .pace .pace-activity{background:#dc3545}.pace-center-atom-danger .pace-progress{height:100px;width:80px}.pace-center-atom-danger .pace-progress::before{background:#dc3545;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-danger .pace-activity{border-color:#dc3545}.pace-center-atom-danger .pace-activity::after,.pace-center-atom-danger .pace-activity::before{border-color:#dc3545}.pace-center-circle-danger .pace .pace-progress{background:rgba(220,53,69,.8);color:#fff}.pace-center-radar-danger .pace .pace-activity{border-color:#dc3545 transparent transparent}.pace-center-radar-danger .pace .pace-activity::before{border-color:#dc3545 transparent transparent}.pace-center-simple-danger .pace{background:#fff;border-color:#dc3545}.pace-center-simple-danger .pace .pace-progress{background:#dc3545}.pace-material-danger .pace{color:#dc3545}.pace-corner-indicator-danger .pace .pace-activity{background:#dc3545}.pace-corner-indicator-danger .pace .pace-activity::after,.pace-corner-indicator-danger .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-danger .pace .pace-activity::before{border-right-color:rgba(220,53,69,.2);border-left-color:rgba(220,53,69,.2)}.pace-corner-indicator-danger .pace .pace-activity::after{border-top-color:rgba(220,53,69,.2);border-bottom-color:rgba(220,53,69,.2)}.pace-fill-left-danger .pace .pace-progress{background-color:rgba(220,53,69,.2)}.pace-flash-danger .pace .pace-progress{background:#dc3545}.pace-flash-danger .pace .pace-progress-inner{box-shadow:0 0 10px #dc3545,0 0 5px #dc3545}.pace-flash-danger .pace .pace-activity{border-top-color:#dc3545;border-left-color:#dc3545}.pace-loading-bar-danger .pace .pace-progress{background:#dc3545;color:#dc3545;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-danger .pace .pace-activity{box-shadow:inset 0 0 0 2px #dc3545,inset 0 0 0 7px #fff}.pace-mac-osx-danger .pace .pace-progress{background-color:#dc3545;box-shadow:inset -1px 0 #dc3545,inset 0 -1px #dc3545,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-danger .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-danger .pace-progress{color:#dc3545}.pace-light .pace .pace-progress{background:#f8f9fa}.pace-barber-shop-light .pace{background:#1f2d3d}.pace-barber-shop-light .pace .pace-progress{background:#f8f9fa}.pace-barber-shop-light .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-light .pace .pace-progress::after{color:rgba(248,249,250,.2)}.pace-bounce-light .pace .pace-activity{background:#f8f9fa}.pace-center-atom-light .pace-progress{height:100px;width:80px}.pace-center-atom-light .pace-progress::before{background:#f8f9fa;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-light .pace-activity{border-color:#f8f9fa}.pace-center-atom-light .pace-activity::after,.pace-center-atom-light .pace-activity::before{border-color:#f8f9fa}.pace-center-circle-light .pace .pace-progress{background:rgba(248,249,250,.8);color:#1f2d3d}.pace-center-radar-light .pace .pace-activity{border-color:#f8f9fa transparent transparent}.pace-center-radar-light .pace .pace-activity::before{border-color:#f8f9fa transparent transparent}.pace-center-simple-light .pace{background:#1f2d3d;border-color:#f8f9fa}.pace-center-simple-light .pace .pace-progress{background:#f8f9fa}.pace-material-light .pace{color:#f8f9fa}.pace-corner-indicator-light .pace .pace-activity{background:#f8f9fa}.pace-corner-indicator-light .pace .pace-activity::after,.pace-corner-indicator-light .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-light .pace .pace-activity::before{border-right-color:rgba(248,249,250,.2);border-left-color:rgba(248,249,250,.2)}.pace-corner-indicator-light .pace .pace-activity::after{border-top-color:rgba(248,249,250,.2);border-bottom-color:rgba(248,249,250,.2)}.pace-fill-left-light .pace .pace-progress{background-color:rgba(248,249,250,.2)}.pace-flash-light .pace .pace-progress{background:#f8f9fa}.pace-flash-light .pace .pace-progress-inner{box-shadow:0 0 10px #f8f9fa,0 0 5px #f8f9fa}.pace-flash-light .pace .pace-activity{border-top-color:#f8f9fa;border-left-color:#f8f9fa}.pace-loading-bar-light .pace .pace-progress{background:#f8f9fa;color:#f8f9fa;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-light .pace .pace-activity{box-shadow:inset 0 0 0 2px #f8f9fa,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-light .pace .pace-progress{background-color:#f8f9fa;box-shadow:inset -1px 0 #f8f9fa,inset 0 -1px #f8f9fa,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-light .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-light .pace-progress{color:#f8f9fa}.pace-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-dark .pace{background:#fff}.pace-barber-shop-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-dark .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-dark .pace .pace-progress::after{color:rgba(52,58,64,.2)}.pace-bounce-dark .pace .pace-activity{background:#343a40}.pace-center-atom-dark .pace-progress{height:100px;width:80px}.pace-center-atom-dark .pace-progress::before{background:#343a40;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-dark .pace-activity{border-color:#343a40}.pace-center-atom-dark .pace-activity::after,.pace-center-atom-dark .pace-activity::before{border-color:#343a40}.pace-center-circle-dark .pace .pace-progress{background:rgba(52,58,64,.8);color:#fff}.pace-center-radar-dark .pace .pace-activity{border-color:#343a40 transparent transparent}.pace-center-radar-dark .pace .pace-activity::before{border-color:#343a40 transparent transparent}.pace-center-simple-dark .pace{background:#fff;border-color:#343a40}.pace-center-simple-dark .pace .pace-progress{background:#343a40}.pace-material-dark .pace{color:#343a40}.pace-corner-indicator-dark .pace .pace-activity{background:#343a40}.pace-corner-indicator-dark .pace .pace-activity::after,.pace-corner-indicator-dark .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-dark .pace .pace-activity::before{border-right-color:rgba(52,58,64,.2);border-left-color:rgba(52,58,64,.2)}.pace-corner-indicator-dark .pace .pace-activity::after{border-top-color:rgba(52,58,64,.2);border-bottom-color:rgba(52,58,64,.2)}.pace-fill-left-dark .pace .pace-progress{background-color:rgba(52,58,64,.2)}.pace-flash-dark .pace .pace-progress{background:#343a40}.pace-flash-dark .pace .pace-progress-inner{box-shadow:0 0 10px #343a40,0 0 5px #343a40}.pace-flash-dark .pace .pace-activity{border-top-color:#343a40;border-left-color:#343a40}.pace-loading-bar-dark .pace .pace-progress{background:#343a40;color:#343a40;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-dark .pace .pace-activity{box-shadow:inset 0 0 0 2px #343a40,inset 0 0 0 7px #fff}.pace-mac-osx-dark .pace .pace-progress{background-color:#343a40;box-shadow:inset -1px 0 #343a40,inset 0 -1px #343a40,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-dark .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-dark .pace-progress{color:#343a40}.pace-lightblue .pace .pace-progress{background:#3c8dbc}.pace-barber-shop-lightblue .pace{background:#fff}.pace-barber-shop-lightblue .pace .pace-progress{background:#3c8dbc}.pace-barber-shop-lightblue .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-lightblue .pace .pace-progress::after{color:rgba(60,141,188,.2)}.pace-bounce-lightblue .pace .pace-activity{background:#3c8dbc}.pace-center-atom-lightblue .pace-progress{height:100px;width:80px}.pace-center-atom-lightblue .pace-progress::before{background:#3c8dbc;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-lightblue .pace-activity{border-color:#3c8dbc}.pace-center-atom-lightblue .pace-activity::after,.pace-center-atom-lightblue .pace-activity::before{border-color:#3c8dbc}.pace-center-circle-lightblue .pace .pace-progress{background:rgba(60,141,188,.8);color:#fff}.pace-center-radar-lightblue .pace .pace-activity{border-color:#3c8dbc transparent transparent}.pace-center-radar-lightblue .pace .pace-activity::before{border-color:#3c8dbc transparent transparent}.pace-center-simple-lightblue .pace{background:#fff;border-color:#3c8dbc}.pace-center-simple-lightblue .pace .pace-progress{background:#3c8dbc}.pace-material-lightblue .pace{color:#3c8dbc}.pace-corner-indicator-lightblue .pace .pace-activity{background:#3c8dbc}.pace-corner-indicator-lightblue .pace .pace-activity::after,.pace-corner-indicator-lightblue .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-lightblue .pace .pace-activity::before{border-right-color:rgba(60,141,188,.2);border-left-color:rgba(60,141,188,.2)}.pace-corner-indicator-lightblue .pace .pace-activity::after{border-top-color:rgba(60,141,188,.2);border-bottom-color:rgba(60,141,188,.2)}.pace-fill-left-lightblue .pace .pace-progress{background-color:rgba(60,141,188,.2)}.pace-flash-lightblue .pace .pace-progress{background:#3c8dbc}.pace-flash-lightblue .pace .pace-progress-inner{box-shadow:0 0 10px #3c8dbc,0 0 5px #3c8dbc}.pace-flash-lightblue .pace .pace-activity{border-top-color:#3c8dbc;border-left-color:#3c8dbc}.pace-loading-bar-lightblue .pace .pace-progress{background:#3c8dbc;color:#3c8dbc;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-lightblue .pace .pace-activity{box-shadow:inset 0 0 0 2px #3c8dbc,inset 0 0 0 7px #fff}.pace-mac-osx-lightblue .pace .pace-progress{background-color:#3c8dbc;box-shadow:inset -1px 0 #3c8dbc,inset 0 -1px #3c8dbc,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-lightblue .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-lightblue .pace-progress{color:#3c8dbc}.pace-navy .pace .pace-progress{background:#001f3f}.pace-barber-shop-navy .pace{background:#fff}.pace-barber-shop-navy .pace .pace-progress{background:#001f3f}.pace-barber-shop-navy .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-navy .pace .pace-progress::after{color:rgba(0,31,63,.2)}.pace-bounce-navy .pace .pace-activity{background:#001f3f}.pace-center-atom-navy .pace-progress{height:100px;width:80px}.pace-center-atom-navy .pace-progress::before{background:#001f3f;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-navy .pace-activity{border-color:#001f3f}.pace-center-atom-navy .pace-activity::after,.pace-center-atom-navy .pace-activity::before{border-color:#001f3f}.pace-center-circle-navy .pace .pace-progress{background:rgba(0,31,63,.8);color:#fff}.pace-center-radar-navy .pace .pace-activity{border-color:#001f3f transparent transparent}.pace-center-radar-navy .pace .pace-activity::before{border-color:#001f3f transparent transparent}.pace-center-simple-navy .pace{background:#fff;border-color:#001f3f}.pace-center-simple-navy .pace .pace-progress{background:#001f3f}.pace-material-navy .pace{color:#001f3f}.pace-corner-indicator-navy .pace .pace-activity{background:#001f3f}.pace-corner-indicator-navy .pace .pace-activity::after,.pace-corner-indicator-navy .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-navy .pace .pace-activity::before{border-right-color:rgba(0,31,63,.2);border-left-color:rgba(0,31,63,.2)}.pace-corner-indicator-navy .pace .pace-activity::after{border-top-color:rgba(0,31,63,.2);border-bottom-color:rgba(0,31,63,.2)}.pace-fill-left-navy .pace .pace-progress{background-color:rgba(0,31,63,.2)}.pace-flash-navy .pace .pace-progress{background:#001f3f}.pace-flash-navy .pace .pace-progress-inner{box-shadow:0 0 10px #001f3f,0 0 5px #001f3f}.pace-flash-navy .pace .pace-activity{border-top-color:#001f3f;border-left-color:#001f3f}.pace-loading-bar-navy .pace .pace-progress{background:#001f3f;color:#001f3f;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-navy .pace .pace-activity{box-shadow:inset 0 0 0 2px #001f3f,inset 0 0 0 7px #fff}.pace-mac-osx-navy .pace .pace-progress{background-color:#001f3f;box-shadow:inset -1px 0 #001f3f,inset 0 -1px #001f3f,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-navy .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-navy .pace-progress{color:#001f3f}.pace-olive .pace .pace-progress{background:#3d9970}.pace-barber-shop-olive .pace{background:#fff}.pace-barber-shop-olive .pace .pace-progress{background:#3d9970}.pace-barber-shop-olive .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-olive .pace .pace-progress::after{color:rgba(61,153,112,.2)}.pace-bounce-olive .pace .pace-activity{background:#3d9970}.pace-center-atom-olive .pace-progress{height:100px;width:80px}.pace-center-atom-olive .pace-progress::before{background:#3d9970;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-olive .pace-activity{border-color:#3d9970}.pace-center-atom-olive .pace-activity::after,.pace-center-atom-olive .pace-activity::before{border-color:#3d9970}.pace-center-circle-olive .pace .pace-progress{background:rgba(61,153,112,.8);color:#fff}.pace-center-radar-olive .pace .pace-activity{border-color:#3d9970 transparent transparent}.pace-center-radar-olive .pace .pace-activity::before{border-color:#3d9970 transparent transparent}.pace-center-simple-olive .pace{background:#fff;border-color:#3d9970}.pace-center-simple-olive .pace .pace-progress{background:#3d9970}.pace-material-olive .pace{color:#3d9970}.pace-corner-indicator-olive .pace .pace-activity{background:#3d9970}.pace-corner-indicator-olive .pace .pace-activity::after,.pace-corner-indicator-olive .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-olive .pace .pace-activity::before{border-right-color:rgba(61,153,112,.2);border-left-color:rgba(61,153,112,.2)}.pace-corner-indicator-olive .pace .pace-activity::after{border-top-color:rgba(61,153,112,.2);border-bottom-color:rgba(61,153,112,.2)}.pace-fill-left-olive .pace .pace-progress{background-color:rgba(61,153,112,.2)}.pace-flash-olive .pace .pace-progress{background:#3d9970}.pace-flash-olive .pace .pace-progress-inner{box-shadow:0 0 10px #3d9970,0 0 5px #3d9970}.pace-flash-olive .pace .pace-activity{border-top-color:#3d9970;border-left-color:#3d9970}.pace-loading-bar-olive .pace .pace-progress{background:#3d9970;color:#3d9970;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-olive .pace .pace-activity{box-shadow:inset 0 0 0 2px #3d9970,inset 0 0 0 7px #fff}.pace-mac-osx-olive .pace .pace-progress{background-color:#3d9970;box-shadow:inset -1px 0 #3d9970,inset 0 -1px #3d9970,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-olive .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-olive .pace-progress{color:#3d9970}.pace-lime .pace .pace-progress{background:#01ff70}.pace-barber-shop-lime .pace{background:#1f2d3d}.pace-barber-shop-lime .pace .pace-progress{background:#01ff70}.pace-barber-shop-lime .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-lime .pace .pace-progress::after{color:rgba(1,255,112,.2)}.pace-bounce-lime .pace .pace-activity{background:#01ff70}.pace-center-atom-lime .pace-progress{height:100px;width:80px}.pace-center-atom-lime .pace-progress::before{background:#01ff70;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-lime .pace-activity{border-color:#01ff70}.pace-center-atom-lime .pace-activity::after,.pace-center-atom-lime .pace-activity::before{border-color:#01ff70}.pace-center-circle-lime .pace .pace-progress{background:rgba(1,255,112,.8);color:#1f2d3d}.pace-center-radar-lime .pace .pace-activity{border-color:#01ff70 transparent transparent}.pace-center-radar-lime .pace .pace-activity::before{border-color:#01ff70 transparent transparent}.pace-center-simple-lime .pace{background:#1f2d3d;border-color:#01ff70}.pace-center-simple-lime .pace .pace-progress{background:#01ff70}.pace-material-lime .pace{color:#01ff70}.pace-corner-indicator-lime .pace .pace-activity{background:#01ff70}.pace-corner-indicator-lime .pace .pace-activity::after,.pace-corner-indicator-lime .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-lime .pace .pace-activity::before{border-right-color:rgba(1,255,112,.2);border-left-color:rgba(1,255,112,.2)}.pace-corner-indicator-lime .pace .pace-activity::after{border-top-color:rgba(1,255,112,.2);border-bottom-color:rgba(1,255,112,.2)}.pace-fill-left-lime .pace .pace-progress{background-color:rgba(1,255,112,.2)}.pace-flash-lime .pace .pace-progress{background:#01ff70}.pace-flash-lime .pace .pace-progress-inner{box-shadow:0 0 10px #01ff70,0 0 5px #01ff70}.pace-flash-lime .pace .pace-activity{border-top-color:#01ff70;border-left-color:#01ff70}.pace-loading-bar-lime .pace .pace-progress{background:#01ff70;color:#01ff70;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-lime .pace .pace-activity{box-shadow:inset 0 0 0 2px #01ff70,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-lime .pace .pace-progress{background-color:#01ff70;box-shadow:inset -1px 0 #01ff70,inset 0 -1px #01ff70,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-lime .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-lime .pace-progress{color:#01ff70}.pace-fuchsia .pace .pace-progress{background:#f012be}.pace-barber-shop-fuchsia .pace{background:#fff}.pace-barber-shop-fuchsia .pace .pace-progress{background:#f012be}.pace-barber-shop-fuchsia .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-fuchsia .pace .pace-progress::after{color:rgba(240,18,190,.2)}.pace-bounce-fuchsia .pace .pace-activity{background:#f012be}.pace-center-atom-fuchsia .pace-progress{height:100px;width:80px}.pace-center-atom-fuchsia .pace-progress::before{background:#f012be;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-fuchsia .pace-activity{border-color:#f012be}.pace-center-atom-fuchsia .pace-activity::after,.pace-center-atom-fuchsia .pace-activity::before{border-color:#f012be}.pace-center-circle-fuchsia .pace .pace-progress{background:rgba(240,18,190,.8);color:#fff}.pace-center-radar-fuchsia .pace .pace-activity{border-color:#f012be transparent transparent}.pace-center-radar-fuchsia .pace .pace-activity::before{border-color:#f012be transparent transparent}.pace-center-simple-fuchsia .pace{background:#fff;border-color:#f012be}.pace-center-simple-fuchsia .pace .pace-progress{background:#f012be}.pace-material-fuchsia .pace{color:#f012be}.pace-corner-indicator-fuchsia .pace .pace-activity{background:#f012be}.pace-corner-indicator-fuchsia .pace .pace-activity::after,.pace-corner-indicator-fuchsia .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-fuchsia .pace .pace-activity::before{border-right-color:rgba(240,18,190,.2);border-left-color:rgba(240,18,190,.2)}.pace-corner-indicator-fuchsia .pace .pace-activity::after{border-top-color:rgba(240,18,190,.2);border-bottom-color:rgba(240,18,190,.2)}.pace-fill-left-fuchsia .pace .pace-progress{background-color:rgba(240,18,190,.2)}.pace-flash-fuchsia .pace .pace-progress{background:#f012be}.pace-flash-fuchsia .pace .pace-progress-inner{box-shadow:0 0 10px #f012be,0 0 5px #f012be}.pace-flash-fuchsia .pace .pace-activity{border-top-color:#f012be;border-left-color:#f012be}.pace-loading-bar-fuchsia .pace .pace-progress{background:#f012be;color:#f012be;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-fuchsia .pace .pace-activity{box-shadow:inset 0 0 0 2px #f012be,inset 0 0 0 7px #fff}.pace-mac-osx-fuchsia .pace .pace-progress{background-color:#f012be;box-shadow:inset -1px 0 #f012be,inset 0 -1px #f012be,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-fuchsia .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-fuchsia .pace-progress{color:#f012be}.pace-maroon .pace .pace-progress{background:#d81b60}.pace-barber-shop-maroon .pace{background:#fff}.pace-barber-shop-maroon .pace .pace-progress{background:#d81b60}.pace-barber-shop-maroon .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-maroon .pace .pace-progress::after{color:rgba(216,27,96,.2)}.pace-bounce-maroon .pace .pace-activity{background:#d81b60}.pace-center-atom-maroon .pace-progress{height:100px;width:80px}.pace-center-atom-maroon .pace-progress::before{background:#d81b60;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-maroon .pace-activity{border-color:#d81b60}.pace-center-atom-maroon .pace-activity::after,.pace-center-atom-maroon .pace-activity::before{border-color:#d81b60}.pace-center-circle-maroon .pace .pace-progress{background:rgba(216,27,96,.8);color:#fff}.pace-center-radar-maroon .pace .pace-activity{border-color:#d81b60 transparent transparent}.pace-center-radar-maroon .pace .pace-activity::before{border-color:#d81b60 transparent transparent}.pace-center-simple-maroon .pace{background:#fff;border-color:#d81b60}.pace-center-simple-maroon .pace .pace-progress{background:#d81b60}.pace-material-maroon .pace{color:#d81b60}.pace-corner-indicator-maroon .pace .pace-activity{background:#d81b60}.pace-corner-indicator-maroon .pace .pace-activity::after,.pace-corner-indicator-maroon .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-maroon .pace .pace-activity::before{border-right-color:rgba(216,27,96,.2);border-left-color:rgba(216,27,96,.2)}.pace-corner-indicator-maroon .pace .pace-activity::after{border-top-color:rgba(216,27,96,.2);border-bottom-color:rgba(216,27,96,.2)}.pace-fill-left-maroon .pace .pace-progress{background-color:rgba(216,27,96,.2)}.pace-flash-maroon .pace .pace-progress{background:#d81b60}.pace-flash-maroon .pace .pace-progress-inner{box-shadow:0 0 10px #d81b60,0 0 5px #d81b60}.pace-flash-maroon .pace .pace-activity{border-top-color:#d81b60;border-left-color:#d81b60}.pace-loading-bar-maroon .pace .pace-progress{background:#d81b60;color:#d81b60;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-maroon .pace .pace-activity{box-shadow:inset 0 0 0 2px #d81b60,inset 0 0 0 7px #fff}.pace-mac-osx-maroon .pace .pace-progress{background-color:#d81b60;box-shadow:inset -1px 0 #d81b60,inset 0 -1px #d81b60,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-maroon .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-maroon .pace-progress{color:#d81b60}.pace-blue .pace .pace-progress{background:#007bff}.pace-barber-shop-blue .pace{background:#fff}.pace-barber-shop-blue .pace .pace-progress{background:#007bff}.pace-barber-shop-blue .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-blue .pace .pace-progress::after{color:rgba(0,123,255,.2)}.pace-bounce-blue .pace .pace-activity{background:#007bff}.pace-center-atom-blue .pace-progress{height:100px;width:80px}.pace-center-atom-blue .pace-progress::before{background:#007bff;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-blue .pace-activity{border-color:#007bff}.pace-center-atom-blue .pace-activity::after,.pace-center-atom-blue .pace-activity::before{border-color:#007bff}.pace-center-circle-blue .pace .pace-progress{background:rgba(0,123,255,.8);color:#fff}.pace-center-radar-blue .pace .pace-activity{border-color:#007bff transparent transparent}.pace-center-radar-blue .pace .pace-activity::before{border-color:#007bff transparent transparent}.pace-center-simple-blue .pace{background:#fff;border-color:#007bff}.pace-center-simple-blue .pace .pace-progress{background:#007bff}.pace-material-blue .pace{color:#007bff}.pace-corner-indicator-blue .pace .pace-activity{background:#007bff}.pace-corner-indicator-blue .pace .pace-activity::after,.pace-corner-indicator-blue .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-blue .pace .pace-activity::before{border-right-color:rgba(0,123,255,.2);border-left-color:rgba(0,123,255,.2)}.pace-corner-indicator-blue .pace .pace-activity::after{border-top-color:rgba(0,123,255,.2);border-bottom-color:rgba(0,123,255,.2)}.pace-fill-left-blue .pace .pace-progress{background-color:rgba(0,123,255,.2)}.pace-flash-blue .pace .pace-progress{background:#007bff}.pace-flash-blue .pace .pace-progress-inner{box-shadow:0 0 10px #007bff,0 0 5px #007bff}.pace-flash-blue .pace .pace-activity{border-top-color:#007bff;border-left-color:#007bff}.pace-loading-bar-blue .pace .pace-progress{background:#007bff;color:#007bff;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-blue .pace .pace-activity{box-shadow:inset 0 0 0 2px #007bff,inset 0 0 0 7px #fff}.pace-mac-osx-blue .pace .pace-progress{background-color:#007bff;box-shadow:inset -1px 0 #007bff,inset 0 -1px #007bff,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-blue .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-blue .pace-progress{color:#007bff}.pace-indigo .pace .pace-progress{background:#6610f2}.pace-barber-shop-indigo .pace{background:#fff}.pace-barber-shop-indigo .pace .pace-progress{background:#6610f2}.pace-barber-shop-indigo .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-indigo .pace .pace-progress::after{color:rgba(102,16,242,.2)}.pace-bounce-indigo .pace .pace-activity{background:#6610f2}.pace-center-atom-indigo .pace-progress{height:100px;width:80px}.pace-center-atom-indigo .pace-progress::before{background:#6610f2;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-indigo .pace-activity{border-color:#6610f2}.pace-center-atom-indigo .pace-activity::after,.pace-center-atom-indigo .pace-activity::before{border-color:#6610f2}.pace-center-circle-indigo .pace .pace-progress{background:rgba(102,16,242,.8);color:#fff}.pace-center-radar-indigo .pace .pace-activity{border-color:#6610f2 transparent transparent}.pace-center-radar-indigo .pace .pace-activity::before{border-color:#6610f2 transparent transparent}.pace-center-simple-indigo .pace{background:#fff;border-color:#6610f2}.pace-center-simple-indigo .pace .pace-progress{background:#6610f2}.pace-material-indigo .pace{color:#6610f2}.pace-corner-indicator-indigo .pace .pace-activity{background:#6610f2}.pace-corner-indicator-indigo .pace .pace-activity::after,.pace-corner-indicator-indigo .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-indigo .pace .pace-activity::before{border-right-color:rgba(102,16,242,.2);border-left-color:rgba(102,16,242,.2)}.pace-corner-indicator-indigo .pace .pace-activity::after{border-top-color:rgba(102,16,242,.2);border-bottom-color:rgba(102,16,242,.2)}.pace-fill-left-indigo .pace .pace-progress{background-color:rgba(102,16,242,.2)}.pace-flash-indigo .pace .pace-progress{background:#6610f2}.pace-flash-indigo .pace .pace-progress-inner{box-shadow:0 0 10px #6610f2,0 0 5px #6610f2}.pace-flash-indigo .pace .pace-activity{border-top-color:#6610f2;border-left-color:#6610f2}.pace-loading-bar-indigo .pace .pace-progress{background:#6610f2;color:#6610f2;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-indigo .pace .pace-activity{box-shadow:inset 0 0 0 2px #6610f2,inset 0 0 0 7px #fff}.pace-mac-osx-indigo .pace .pace-progress{background-color:#6610f2;box-shadow:inset -1px 0 #6610f2,inset 0 -1px #6610f2,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-indigo .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-indigo .pace-progress{color:#6610f2}.pace-purple .pace .pace-progress{background:#6f42c1}.pace-barber-shop-purple .pace{background:#fff}.pace-barber-shop-purple .pace .pace-progress{background:#6f42c1}.pace-barber-shop-purple .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-purple .pace .pace-progress::after{color:rgba(111,66,193,.2)}.pace-bounce-purple .pace .pace-activity{background:#6f42c1}.pace-center-atom-purple .pace-progress{height:100px;width:80px}.pace-center-atom-purple .pace-progress::before{background:#6f42c1;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-purple .pace-activity{border-color:#6f42c1}.pace-center-atom-purple .pace-activity::after,.pace-center-atom-purple .pace-activity::before{border-color:#6f42c1}.pace-center-circle-purple .pace .pace-progress{background:rgba(111,66,193,.8);color:#fff}.pace-center-radar-purple .pace .pace-activity{border-color:#6f42c1 transparent transparent}.pace-center-radar-purple .pace .pace-activity::before{border-color:#6f42c1 transparent transparent}.pace-center-simple-purple .pace{background:#fff;border-color:#6f42c1}.pace-center-simple-purple .pace .pace-progress{background:#6f42c1}.pace-material-purple .pace{color:#6f42c1}.pace-corner-indicator-purple .pace .pace-activity{background:#6f42c1}.pace-corner-indicator-purple .pace .pace-activity::after,.pace-corner-indicator-purple .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-purple .pace .pace-activity::before{border-right-color:rgba(111,66,193,.2);border-left-color:rgba(111,66,193,.2)}.pace-corner-indicator-purple .pace .pace-activity::after{border-top-color:rgba(111,66,193,.2);border-bottom-color:rgba(111,66,193,.2)}.pace-fill-left-purple .pace .pace-progress{background-color:rgba(111,66,193,.2)}.pace-flash-purple .pace .pace-progress{background:#6f42c1}.pace-flash-purple .pace .pace-progress-inner{box-shadow:0 0 10px #6f42c1,0 0 5px #6f42c1}.pace-flash-purple .pace .pace-activity{border-top-color:#6f42c1;border-left-color:#6f42c1}.pace-loading-bar-purple .pace .pace-progress{background:#6f42c1;color:#6f42c1;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-purple .pace .pace-activity{box-shadow:inset 0 0 0 2px #6f42c1,inset 0 0 0 7px #fff}.pace-mac-osx-purple .pace .pace-progress{background-color:#6f42c1;box-shadow:inset -1px 0 #6f42c1,inset 0 -1px #6f42c1,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-purple .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-purple .pace-progress{color:#6f42c1}.pace-pink .pace .pace-progress{background:#e83e8c}.pace-barber-shop-pink .pace{background:#fff}.pace-barber-shop-pink .pace .pace-progress{background:#e83e8c}.pace-barber-shop-pink .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-pink .pace .pace-progress::after{color:rgba(232,62,140,.2)}.pace-bounce-pink .pace .pace-activity{background:#e83e8c}.pace-center-atom-pink .pace-progress{height:100px;width:80px}.pace-center-atom-pink .pace-progress::before{background:#e83e8c;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-pink .pace-activity{border-color:#e83e8c}.pace-center-atom-pink .pace-activity::after,.pace-center-atom-pink .pace-activity::before{border-color:#e83e8c}.pace-center-circle-pink .pace .pace-progress{background:rgba(232,62,140,.8);color:#fff}.pace-center-radar-pink .pace .pace-activity{border-color:#e83e8c transparent transparent}.pace-center-radar-pink .pace .pace-activity::before{border-color:#e83e8c transparent transparent}.pace-center-simple-pink .pace{background:#fff;border-color:#e83e8c}.pace-center-simple-pink .pace .pace-progress{background:#e83e8c}.pace-material-pink .pace{color:#e83e8c}.pace-corner-indicator-pink .pace .pace-activity{background:#e83e8c}.pace-corner-indicator-pink .pace .pace-activity::after,.pace-corner-indicator-pink .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-pink .pace .pace-activity::before{border-right-color:rgba(232,62,140,.2);border-left-color:rgba(232,62,140,.2)}.pace-corner-indicator-pink .pace .pace-activity::after{border-top-color:rgba(232,62,140,.2);border-bottom-color:rgba(232,62,140,.2)}.pace-fill-left-pink .pace .pace-progress{background-color:rgba(232,62,140,.2)}.pace-flash-pink .pace .pace-progress{background:#e83e8c}.pace-flash-pink .pace .pace-progress-inner{box-shadow:0 0 10px #e83e8c,0 0 5px #e83e8c}.pace-flash-pink .pace .pace-activity{border-top-color:#e83e8c;border-left-color:#e83e8c}.pace-loading-bar-pink .pace .pace-progress{background:#e83e8c;color:#e83e8c;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-pink .pace .pace-activity{box-shadow:inset 0 0 0 2px #e83e8c,inset 0 0 0 7px #fff}.pace-mac-osx-pink .pace .pace-progress{background-color:#e83e8c;box-shadow:inset -1px 0 #e83e8c,inset 0 -1px #e83e8c,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-pink .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-pink .pace-progress{color:#e83e8c}.pace-red .pace .pace-progress{background:#dc3545}.pace-barber-shop-red .pace{background:#fff}.pace-barber-shop-red .pace .pace-progress{background:#dc3545}.pace-barber-shop-red .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-red .pace .pace-progress::after{color:rgba(220,53,69,.2)}.pace-bounce-red .pace .pace-activity{background:#dc3545}.pace-center-atom-red .pace-progress{height:100px;width:80px}.pace-center-atom-red .pace-progress::before{background:#dc3545;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-red .pace-activity{border-color:#dc3545}.pace-center-atom-red .pace-activity::after,.pace-center-atom-red .pace-activity::before{border-color:#dc3545}.pace-center-circle-red .pace .pace-progress{background:rgba(220,53,69,.8);color:#fff}.pace-center-radar-red .pace .pace-activity{border-color:#dc3545 transparent transparent}.pace-center-radar-red .pace .pace-activity::before{border-color:#dc3545 transparent transparent}.pace-center-simple-red .pace{background:#fff;border-color:#dc3545}.pace-center-simple-red .pace .pace-progress{background:#dc3545}.pace-material-red .pace{color:#dc3545}.pace-corner-indicator-red .pace .pace-activity{background:#dc3545}.pace-corner-indicator-red .pace .pace-activity::after,.pace-corner-indicator-red .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-red .pace .pace-activity::before{border-right-color:rgba(220,53,69,.2);border-left-color:rgba(220,53,69,.2)}.pace-corner-indicator-red .pace .pace-activity::after{border-top-color:rgba(220,53,69,.2);border-bottom-color:rgba(220,53,69,.2)}.pace-fill-left-red .pace .pace-progress{background-color:rgba(220,53,69,.2)}.pace-flash-red .pace .pace-progress{background:#dc3545}.pace-flash-red .pace .pace-progress-inner{box-shadow:0 0 10px #dc3545,0 0 5px #dc3545}.pace-flash-red .pace .pace-activity{border-top-color:#dc3545;border-left-color:#dc3545}.pace-loading-bar-red .pace .pace-progress{background:#dc3545;color:#dc3545;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-red .pace .pace-activity{box-shadow:inset 0 0 0 2px #dc3545,inset 0 0 0 7px #fff}.pace-mac-osx-red .pace .pace-progress{background-color:#dc3545;box-shadow:inset -1px 0 #dc3545,inset 0 -1px #dc3545,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-red .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-red .pace-progress{color:#dc3545}.pace-orange .pace .pace-progress{background:#fd7e14}.pace-barber-shop-orange .pace{background:#1f2d3d}.pace-barber-shop-orange .pace .pace-progress{background:#fd7e14}.pace-barber-shop-orange .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-orange .pace .pace-progress::after{color:rgba(253,126,20,.2)}.pace-bounce-orange .pace .pace-activity{background:#fd7e14}.pace-center-atom-orange .pace-progress{height:100px;width:80px}.pace-center-atom-orange .pace-progress::before{background:#fd7e14;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-orange .pace-activity{border-color:#fd7e14}.pace-center-atom-orange .pace-activity::after,.pace-center-atom-orange .pace-activity::before{border-color:#fd7e14}.pace-center-circle-orange .pace .pace-progress{background:rgba(253,126,20,.8);color:#1f2d3d}.pace-center-radar-orange .pace .pace-activity{border-color:#fd7e14 transparent transparent}.pace-center-radar-orange .pace .pace-activity::before{border-color:#fd7e14 transparent transparent}.pace-center-simple-orange .pace{background:#1f2d3d;border-color:#fd7e14}.pace-center-simple-orange .pace .pace-progress{background:#fd7e14}.pace-material-orange .pace{color:#fd7e14}.pace-corner-indicator-orange .pace .pace-activity{background:#fd7e14}.pace-corner-indicator-orange .pace .pace-activity::after,.pace-corner-indicator-orange .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-orange .pace .pace-activity::before{border-right-color:rgba(253,126,20,.2);border-left-color:rgba(253,126,20,.2)}.pace-corner-indicator-orange .pace .pace-activity::after{border-top-color:rgba(253,126,20,.2);border-bottom-color:rgba(253,126,20,.2)}.pace-fill-left-orange .pace .pace-progress{background-color:rgba(253,126,20,.2)}.pace-flash-orange .pace .pace-progress{background:#fd7e14}.pace-flash-orange .pace .pace-progress-inner{box-shadow:0 0 10px #fd7e14,0 0 5px #fd7e14}.pace-flash-orange .pace .pace-activity{border-top-color:#fd7e14;border-left-color:#fd7e14}.pace-loading-bar-orange .pace .pace-progress{background:#fd7e14;color:#fd7e14;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-orange .pace .pace-activity{box-shadow:inset 0 0 0 2px #fd7e14,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-orange .pace .pace-progress{background-color:#fd7e14;box-shadow:inset -1px 0 #fd7e14,inset 0 -1px #fd7e14,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-orange .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-orange .pace-progress{color:#fd7e14}.pace-yellow .pace .pace-progress{background:#ffc107}.pace-barber-shop-yellow .pace{background:#1f2d3d}.pace-barber-shop-yellow .pace .pace-progress{background:#ffc107}.pace-barber-shop-yellow .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-yellow .pace .pace-progress::after{color:rgba(255,193,7,.2)}.pace-bounce-yellow .pace .pace-activity{background:#ffc107}.pace-center-atom-yellow .pace-progress{height:100px;width:80px}.pace-center-atom-yellow .pace-progress::before{background:#ffc107;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-yellow .pace-activity{border-color:#ffc107}.pace-center-atom-yellow .pace-activity::after,.pace-center-atom-yellow .pace-activity::before{border-color:#ffc107}.pace-center-circle-yellow .pace .pace-progress{background:rgba(255,193,7,.8);color:#1f2d3d}.pace-center-radar-yellow .pace .pace-activity{border-color:#ffc107 transparent transparent}.pace-center-radar-yellow .pace .pace-activity::before{border-color:#ffc107 transparent transparent}.pace-center-simple-yellow .pace{background:#1f2d3d;border-color:#ffc107}.pace-center-simple-yellow .pace .pace-progress{background:#ffc107}.pace-material-yellow .pace{color:#ffc107}.pace-corner-indicator-yellow .pace .pace-activity{background:#ffc107}.pace-corner-indicator-yellow .pace .pace-activity::after,.pace-corner-indicator-yellow .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-yellow .pace .pace-activity::before{border-right-color:rgba(255,193,7,.2);border-left-color:rgba(255,193,7,.2)}.pace-corner-indicator-yellow .pace .pace-activity::after{border-top-color:rgba(255,193,7,.2);border-bottom-color:rgba(255,193,7,.2)}.pace-fill-left-yellow .pace .pace-progress{background-color:rgba(255,193,7,.2)}.pace-flash-yellow .pace .pace-progress{background:#ffc107}.pace-flash-yellow .pace .pace-progress-inner{box-shadow:0 0 10px #ffc107,0 0 5px #ffc107}.pace-flash-yellow .pace .pace-activity{border-top-color:#ffc107;border-left-color:#ffc107}.pace-loading-bar-yellow .pace .pace-progress{background:#ffc107;color:#ffc107;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-yellow .pace .pace-activity{box-shadow:inset 0 0 0 2px #ffc107,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-yellow .pace .pace-progress{background-color:#ffc107;box-shadow:inset -1px 0 #ffc107,inset 0 -1px #ffc107,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-yellow .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-yellow .pace-progress{color:#ffc107}.pace-green .pace .pace-progress{background:#28a745}.pace-barber-shop-green .pace{background:#fff}.pace-barber-shop-green .pace .pace-progress{background:#28a745}.pace-barber-shop-green .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-green .pace .pace-progress::after{color:rgba(40,167,69,.2)}.pace-bounce-green .pace .pace-activity{background:#28a745}.pace-center-atom-green .pace-progress{height:100px;width:80px}.pace-center-atom-green .pace-progress::before{background:#28a745;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-green .pace-activity{border-color:#28a745}.pace-center-atom-green .pace-activity::after,.pace-center-atom-green .pace-activity::before{border-color:#28a745}.pace-center-circle-green .pace .pace-progress{background:rgba(40,167,69,.8);color:#fff}.pace-center-radar-green .pace .pace-activity{border-color:#28a745 transparent transparent}.pace-center-radar-green .pace .pace-activity::before{border-color:#28a745 transparent transparent}.pace-center-simple-green .pace{background:#fff;border-color:#28a745}.pace-center-simple-green .pace .pace-progress{background:#28a745}.pace-material-green .pace{color:#28a745}.pace-corner-indicator-green .pace .pace-activity{background:#28a745}.pace-corner-indicator-green .pace .pace-activity::after,.pace-corner-indicator-green .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-green .pace .pace-activity::before{border-right-color:rgba(40,167,69,.2);border-left-color:rgba(40,167,69,.2)}.pace-corner-indicator-green .pace .pace-activity::after{border-top-color:rgba(40,167,69,.2);border-bottom-color:rgba(40,167,69,.2)}.pace-fill-left-green .pace .pace-progress{background-color:rgba(40,167,69,.2)}.pace-flash-green .pace .pace-progress{background:#28a745}.pace-flash-green .pace .pace-progress-inner{box-shadow:0 0 10px #28a745,0 0 5px #28a745}.pace-flash-green .pace .pace-activity{border-top-color:#28a745;border-left-color:#28a745}.pace-loading-bar-green .pace .pace-progress{background:#28a745;color:#28a745;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-green .pace .pace-activity{box-shadow:inset 0 0 0 2px #28a745,inset 0 0 0 7px #fff}.pace-mac-osx-green .pace .pace-progress{background-color:#28a745;box-shadow:inset -1px 0 #28a745,inset 0 -1px #28a745,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-green .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-green .pace-progress{color:#28a745}.pace-teal .pace .pace-progress{background:#20c997}.pace-barber-shop-teal .pace{background:#fff}.pace-barber-shop-teal .pace .pace-progress{background:#20c997}.pace-barber-shop-teal .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-teal .pace .pace-progress::after{color:rgba(32,201,151,.2)}.pace-bounce-teal .pace .pace-activity{background:#20c997}.pace-center-atom-teal .pace-progress{height:100px;width:80px}.pace-center-atom-teal .pace-progress::before{background:#20c997;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-teal .pace-activity{border-color:#20c997}.pace-center-atom-teal .pace-activity::after,.pace-center-atom-teal .pace-activity::before{border-color:#20c997}.pace-center-circle-teal .pace .pace-progress{background:rgba(32,201,151,.8);color:#fff}.pace-center-radar-teal .pace .pace-activity{border-color:#20c997 transparent transparent}.pace-center-radar-teal .pace .pace-activity::before{border-color:#20c997 transparent transparent}.pace-center-simple-teal .pace{background:#fff;border-color:#20c997}.pace-center-simple-teal .pace .pace-progress{background:#20c997}.pace-material-teal .pace{color:#20c997}.pace-corner-indicator-teal .pace .pace-activity{background:#20c997}.pace-corner-indicator-teal .pace .pace-activity::after,.pace-corner-indicator-teal .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-teal .pace .pace-activity::before{border-right-color:rgba(32,201,151,.2);border-left-color:rgba(32,201,151,.2)}.pace-corner-indicator-teal .pace .pace-activity::after{border-top-color:rgba(32,201,151,.2);border-bottom-color:rgba(32,201,151,.2)}.pace-fill-left-teal .pace .pace-progress{background-color:rgba(32,201,151,.2)}.pace-flash-teal .pace .pace-progress{background:#20c997}.pace-flash-teal .pace .pace-progress-inner{box-shadow:0 0 10px #20c997,0 0 5px #20c997}.pace-flash-teal .pace .pace-activity{border-top-color:#20c997;border-left-color:#20c997}.pace-loading-bar-teal .pace .pace-progress{background:#20c997;color:#20c997;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-teal .pace .pace-activity{box-shadow:inset 0 0 0 2px #20c997,inset 0 0 0 7px #fff}.pace-mac-osx-teal .pace .pace-progress{background-color:#20c997;box-shadow:inset -1px 0 #20c997,inset 0 -1px #20c997,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-teal .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-teal .pace-progress{color:#20c997}.pace-cyan .pace .pace-progress{background:#17a2b8}.pace-barber-shop-cyan .pace{background:#fff}.pace-barber-shop-cyan .pace .pace-progress{background:#17a2b8}.pace-barber-shop-cyan .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-cyan .pace .pace-progress::after{color:rgba(23,162,184,.2)}.pace-bounce-cyan .pace .pace-activity{background:#17a2b8}.pace-center-atom-cyan .pace-progress{height:100px;width:80px}.pace-center-atom-cyan .pace-progress::before{background:#17a2b8;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-cyan .pace-activity{border-color:#17a2b8}.pace-center-atom-cyan .pace-activity::after,.pace-center-atom-cyan .pace-activity::before{border-color:#17a2b8}.pace-center-circle-cyan .pace .pace-progress{background:rgba(23,162,184,.8);color:#fff}.pace-center-radar-cyan .pace .pace-activity{border-color:#17a2b8 transparent transparent}.pace-center-radar-cyan .pace .pace-activity::before{border-color:#17a2b8 transparent transparent}.pace-center-simple-cyan .pace{background:#fff;border-color:#17a2b8}.pace-center-simple-cyan .pace .pace-progress{background:#17a2b8}.pace-material-cyan .pace{color:#17a2b8}.pace-corner-indicator-cyan .pace .pace-activity{background:#17a2b8}.pace-corner-indicator-cyan .pace .pace-activity::after,.pace-corner-indicator-cyan .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-cyan .pace .pace-activity::before{border-right-color:rgba(23,162,184,.2);border-left-color:rgba(23,162,184,.2)}.pace-corner-indicator-cyan .pace .pace-activity::after{border-top-color:rgba(23,162,184,.2);border-bottom-color:rgba(23,162,184,.2)}.pace-fill-left-cyan .pace .pace-progress{background-color:rgba(23,162,184,.2)}.pace-flash-cyan .pace .pace-progress{background:#17a2b8}.pace-flash-cyan .pace .pace-progress-inner{box-shadow:0 0 10px #17a2b8,0 0 5px #17a2b8}.pace-flash-cyan .pace .pace-activity{border-top-color:#17a2b8;border-left-color:#17a2b8}.pace-loading-bar-cyan .pace .pace-progress{background:#17a2b8;color:#17a2b8;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-cyan .pace .pace-activity{box-shadow:inset 0 0 0 2px #17a2b8,inset 0 0 0 7px #fff}.pace-mac-osx-cyan .pace .pace-progress{background-color:#17a2b8;box-shadow:inset -1px 0 #17a2b8,inset 0 -1px #17a2b8,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-cyan .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-cyan .pace-progress{color:#17a2b8}.pace-white .pace .pace-progress{background:#fff}.pace-barber-shop-white .pace{background:#1f2d3d}.pace-barber-shop-white .pace .pace-progress{background:#fff}.pace-barber-shop-white .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-white .pace .pace-progress::after{color:rgba(255,255,255,.2)}.pace-bounce-white .pace .pace-activity{background:#fff}.pace-center-atom-white .pace-progress{height:100px;width:80px}.pace-center-atom-white .pace-progress::before{background:#fff;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-white .pace-activity{border-color:#fff}.pace-center-atom-white .pace-activity::after,.pace-center-atom-white .pace-activity::before{border-color:#fff}.pace-center-circle-white .pace .pace-progress{background:rgba(255,255,255,.8);color:#1f2d3d}.pace-center-radar-white .pace .pace-activity{border-color:#fff transparent transparent}.pace-center-radar-white .pace .pace-activity::before{border-color:#fff transparent transparent}.pace-center-simple-white .pace{background:#1f2d3d;border-color:#fff}.pace-center-simple-white .pace .pace-progress{background:#fff}.pace-material-white .pace{color:#fff}.pace-corner-indicator-white .pace .pace-activity{background:#fff}.pace-corner-indicator-white .pace .pace-activity::after,.pace-corner-indicator-white .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-white .pace .pace-activity::before{border-right-color:rgba(255,255,255,.2);border-left-color:rgba(255,255,255,.2)}.pace-corner-indicator-white .pace .pace-activity::after{border-top-color:rgba(255,255,255,.2);border-bottom-color:rgba(255,255,255,.2)}.pace-fill-left-white .pace .pace-progress{background-color:rgba(255,255,255,.2)}.pace-flash-white .pace .pace-progress{background:#fff}.pace-flash-white .pace .pace-progress-inner{box-shadow:0 0 10px #fff,0 0 5px #fff}.pace-flash-white .pace .pace-activity{border-top-color:#fff;border-left-color:#fff}.pace-loading-bar-white .pace .pace-progress{background:#fff;color:#fff;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-white .pace .pace-activity{box-shadow:inset 0 0 0 2px #fff,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-white .pace .pace-progress{background-color:#fff;box-shadow:inset -1px 0 #fff,inset 0 -1px #fff,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-white .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-white .pace-progress{color:#fff}.pace-gray .pace .pace-progress{background:#6c757d}.pace-barber-shop-gray .pace{background:#fff}.pace-barber-shop-gray .pace .pace-progress{background:#6c757d}.pace-barber-shop-gray .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-gray .pace .pace-progress::after{color:rgba(108,117,125,.2)}.pace-bounce-gray .pace .pace-activity{background:#6c757d}.pace-center-atom-gray .pace-progress{height:100px;width:80px}.pace-center-atom-gray .pace-progress::before{background:#6c757d;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-gray .pace-activity{border-color:#6c757d}.pace-center-atom-gray .pace-activity::after,.pace-center-atom-gray .pace-activity::before{border-color:#6c757d}.pace-center-circle-gray .pace .pace-progress{background:rgba(108,117,125,.8);color:#fff}.pace-center-radar-gray .pace .pace-activity{border-color:#6c757d transparent transparent}.pace-center-radar-gray .pace .pace-activity::before{border-color:#6c757d transparent transparent}.pace-center-simple-gray .pace{background:#fff;border-color:#6c757d}.pace-center-simple-gray .pace .pace-progress{background:#6c757d}.pace-material-gray .pace{color:#6c757d}.pace-corner-indicator-gray .pace .pace-activity{background:#6c757d}.pace-corner-indicator-gray .pace .pace-activity::after,.pace-corner-indicator-gray .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-gray .pace .pace-activity::before{border-right-color:rgba(108,117,125,.2);border-left-color:rgba(108,117,125,.2)}.pace-corner-indicator-gray .pace .pace-activity::after{border-top-color:rgba(108,117,125,.2);border-bottom-color:rgba(108,117,125,.2)}.pace-fill-left-gray .pace .pace-progress{background-color:rgba(108,117,125,.2)}.pace-flash-gray .pace .pace-progress{background:#6c757d}.pace-flash-gray .pace .pace-progress-inner{box-shadow:0 0 10px #6c757d,0 0 5px #6c757d}.pace-flash-gray .pace .pace-activity{border-top-color:#6c757d;border-left-color:#6c757d}.pace-loading-bar-gray .pace .pace-progress{background:#6c757d;color:#6c757d;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-gray .pace .pace-activity{box-shadow:inset 0 0 0 2px #6c757d,inset 0 0 0 7px #fff}.pace-mac-osx-gray .pace .pace-progress{background-color:#6c757d;box-shadow:inset -1px 0 #6c757d,inset 0 -1px #6c757d,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-gray .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-gray .pace-progress{color:#6c757d}.pace-gray-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-gray-dark .pace{background:#fff}.pace-barber-shop-gray-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-gray-dark .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-gray-dark .pace .pace-progress::after{color:rgba(52,58,64,.2)}.pace-bounce-gray-dark .pace .pace-activity{background:#343a40}.pace-center-atom-gray-dark .pace-progress{height:100px;width:80px}.pace-center-atom-gray-dark .pace-progress::before{background:#343a40;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-gray-dark .pace-activity{border-color:#343a40}.pace-center-atom-gray-dark .pace-activity::after,.pace-center-atom-gray-dark .pace-activity::before{border-color:#343a40}.pace-center-circle-gray-dark .pace .pace-progress{background:rgba(52,58,64,.8);color:#fff}.pace-center-radar-gray-dark .pace .pace-activity{border-color:#343a40 transparent transparent}.pace-center-radar-gray-dark .pace .pace-activity::before{border-color:#343a40 transparent transparent}.pace-center-simple-gray-dark .pace{background:#fff;border-color:#343a40}.pace-center-simple-gray-dark .pace .pace-progress{background:#343a40}.pace-material-gray-dark .pace{color:#343a40}.pace-corner-indicator-gray-dark .pace .pace-activity{background:#343a40}.pace-corner-indicator-gray-dark .pace .pace-activity::after,.pace-corner-indicator-gray-dark .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-gray-dark .pace .pace-activity::before{border-right-color:rgba(52,58,64,.2);border-left-color:rgba(52,58,64,.2)}.pace-corner-indicator-gray-dark .pace .pace-activity::after{border-top-color:rgba(52,58,64,.2);border-bottom-color:rgba(52,58,64,.2)}.pace-fill-left-gray-dark .pace .pace-progress{background-color:rgba(52,58,64,.2)}.pace-flash-gray-dark .pace .pace-progress{background:#343a40}.pace-flash-gray-dark .pace .pace-progress-inner{box-shadow:0 0 10px #343a40,0 0 5px #343a40}.pace-flash-gray-dark .pace .pace-activity{border-top-color:#343a40;border-left-color:#343a40}.pace-loading-bar-gray-dark .pace .pace-progress{background:#343a40;color:#343a40;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-gray-dark .pace .pace-activity{box-shadow:inset 0 0 0 2px #343a40,inset 0 0 0 7px #fff}.pace-mac-osx-gray-dark .pace .pace-progress{background-color:#343a40;box-shadow:inset -1px 0 #343a40,inset 0 -1px #343a40,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-gray-dark .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-gray-dark .pace-progress{color:#343a40}.bootstrap-switch{border:1px solid #ced4da;border-radius:.25rem;cursor:pointer;direction:ltr;display:inline-block;line-height:.5rem;overflow:hidden;position:relative;text-align:left;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;z-index:0}.bootstrap-switch .bootstrap-switch-container{border-radius:.25rem;display:inline-block;top:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch:focus-within{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{box-sizing:border-box;cursor:pointer;display:table-cell;font-size:1rem;font-weight:500;line-height:1.2rem;padding:.25rem .5rem;vertical-align:middle}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{background:#e9ecef;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{background:#007bff;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-secondary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-secondary{background:#6c757d;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{background:#28a745;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{background:#17a2b8;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#ffc107;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{background:#dc3545;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-light,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-light{background:#f8f9fa;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-dark,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-dark{background:#343a40;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-lightblue,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-lightblue{background:#3c8dbc;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-navy,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-navy{background:#001f3f;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-olive,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-olive{background:#3d9970;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-lime,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-lime{background:#01ff70;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-fuchsia,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-fuchsia{background:#f012be;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-maroon,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-maroon{background:#d81b60;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-blue,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-blue{background:#007bff;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-indigo,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-indigo{background:#6610f2;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-purple,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-purple{background:#6f42c1;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-pink,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-pink{background:#e83e8c;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-red,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-red{background:#dc3545;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-orange,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-orange{background:#fd7e14;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-yellow,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-yellow{background:#ffc107;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-green,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-green{background:#28a745;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-teal,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-teal{background:#20c997;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-cyan,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-cyan{background:#17a2b8;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-white,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-white{background:#fff;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-gray,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-gray{background:#6c757d;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-gray-dark,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-gray-dark{background:#343a40;color:#fff}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:.1rem;border-top-left-radius:.1rem}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:.1rem;border-top-right-radius:.1rem}.bootstrap-switch input[type=checkbox],.bootstrap-switch input[type=radio]{left:0;margin:0;opacity:0;position:absolute;top:0;visibility:hidden;z-index:-1}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{font-size:.875rem;line-height:1.5;padding:.1rem .3rem}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{font-size:.875rem;line-height:1.5;padding:.2rem .4rem}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{font-size:1.25rem;line-height:1.3333333rem;padding:.3rem .5rem}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{cursor:default;opacity:.5}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 .1rem .1rem 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:.1rem 0 0 .1rem}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:.1rem;border-top-right-radius:.1rem}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:.1rem;border-top-left-radius:.1rem}.dark-mode .bootstrap-switch{border-color:#6c757d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{background-color:#3a4047;color:#fff;border-color:#454d55}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{background:#3f6791;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-secondary,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-secondary{background:#6c757d;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{background:#00bc8c;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{background:#3498db;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#f39c12;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{background:#e74c3c;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-light,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-light{background:#f8f9fa;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-dark,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-dark{background:#343a40;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-lightblue,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-lightblue{background:#86bad8;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-navy,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-navy{background:#002c59;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-olive,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-olive{background:#74c8a3;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-lime,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-lime{background:#67ffa9;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-fuchsia,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-fuchsia{background:#f672d8;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-maroon,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-maroon{background:#ed6c9b;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-blue,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-blue{background:#3f6791;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-indigo,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-indigo{background:#6610f2;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-purple,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-purple{background:#6f42c1;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-pink,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-pink{background:#e83e8c;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-red,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-red{background:#e74c3c;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-orange,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-orange{background:#fd7e14;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-yellow,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-yellow{background:#f39c12;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-green,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-green{background:#00bc8c;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-teal,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-teal{background:#20c997;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-cyan,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-cyan{background:#3498db;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-white,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-white{background:#fff;color:#1f2d3d}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-gray,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-gray{background:#6c757d;color:#fff}.dark-mode .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-gray-dark,.dark-mode .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-gray-dark{background:#343a40;color:#fff}.jqstooltip{height:auto!important;padding:5px!important;width:auto!important}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f8f9fa;border:1px dashed #dee2e6;margin-bottom:10px}.chart{overflow:hidden;position:relative}.dark-mode .irs--flat .irs-line{background-color:#4b545c}.dark-mode .jsgrid-alt-row>.jsgrid-cell,.dark-mode .jsgrid-edit-row>.jsgrid-cell,.dark-mode .jsgrid-filter-row>.jsgrid-cell,.dark-mode .jsgrid-grid-body,.dark-mode .jsgrid-grid-header,.dark-mode .jsgrid-header-row>.jsgrid-header-cell,.dark-mode .jsgrid-insert-row>.jsgrid-cell,.dark-mode .jsgrid-row>.jsgrid-cell{border-color:#6c757d}.dark-mode .jsgrid-header-row>.jsgrid-header-cell,.dark-mode .jsgrid-row>.jsgrid-cell{background-color:#343a40}.dark-mode .jsgrid-alt-row>.jsgrid-cell{background-color:#3a4047}.dark-mode .jsgrid-selected-row>.jsgrid-cell{background-color:#3f474e}.border-transparent{border-color:transparent!important}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{font-size:16px;font-weight:600;margin:0;padding:0}.description-block>.description-text{text-transform:uppercase}.description-block .description-icon{font-size:16px}.list-group-unbordered>.list-group-item{border-left:0;border-radius:0;border-right:0;padding-left:0;padding-right:0}.list-header{color:#6c757d;font-size:15px;font-weight:700;padding:10px 4px}.list-seperator{background-color:rgba(0,0,0,.125);height:1px;margin:15px 0 9px}.list-link>a{color:#6c757d;padding:4px}.list-link>a:hover{color:#212529}.user-block{float:left}.user-block img{float:left;height:40px;width:40px}.user-block .comment,.user-block .description,.user-block .username{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600;margin-top:-1px}.user-block .description{color:#6c757d;font-size:13px;margin-top:-3px}.user-block.user-block-sm img{width:1.875rem;height:1.875rem}.user-block.user-block-sm .comment,.user-block.user-block-sm .description,.user-block.user-block-sm .username{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-lg,.img-md,.img-sm{float:left}.img-sm{height:1.875rem;width:1.875rem}.img-sm+.img-push{margin-left:2.5rem}.img-md{width:3.75rem;height:3.75rem}.img-md+.img-push{margin-left:4.375rem}.img-lg{width:6.25rem;height:6.25rem}.img-lg+.img-push{margin-left:6.875rem}.img-bordered{border:3px solid #adb5bd;padding:3px}.img-bordered-sm{border:2px solid #adb5bd;padding:2px}.img-rounded{border-radius:.25rem}.img-circle{border-radius:50%}.img-size-32,.img-size-50,.img-size-64{height:auto}.img-size-64{width:64px}.img-size-50{width:50px}.img-size-32{width:32px}.size-32,.size-40,.size-50{display:block;text-align:center}.size-32{height:32px;line-height:32px;width:32px}.size-40{height:40px;line-height:40px;width:40px}.size-50{height:50px;line-height:50px;width:50px}.attachment-block{background-color:#f8f9fa;border:1px solid rgba(0,0,0,.125);margin-bottom:10px;padding:5px}.attachment-block .attachment-img{float:left;height:auto;max-height:100px;max-width:100px}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#495057}.card>.loading-img,.card>.overlay,.info-box>.loading-img,.info-box>.overlay,.overlay-wrapper>.loading-img,.overlay-wrapper>.overlay,.small-box>.loading-img,.small-box>.overlay{height:100%;left:0;position:absolute;top:0;width:100%}.card .overlay,.info-box .overlay,.overlay-wrapper .overlay,.small-box .overlay{border-radius:.25rem;-webkit-align-items:center;-ms-flex-align:center;align-items:center;background-color:rgba(255,255,255,.7);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;z-index:50}.card .overlay>.fa,.card .overlay>.fab,.card .overlay>.fad,.card .overlay>.fal,.card .overlay>.far,.card .overlay>.fas,.card .overlay>.ion,.card .overlay>.svg-inline--fa,.info-box .overlay>.fa,.info-box .overlay>.fab,.info-box .overlay>.fad,.info-box .overlay>.fal,.info-box .overlay>.far,.info-box .overlay>.fas,.info-box .overlay>.ion,.info-box .overlay>.svg-inline--fa,.overlay-wrapper .overlay>.fa,.overlay-wrapper .overlay>.fab,.overlay-wrapper .overlay>.fad,.overlay-wrapper .overlay>.fal,.overlay-wrapper .overlay>.far,.overlay-wrapper .overlay>.fas,.overlay-wrapper .overlay>.ion,.overlay-wrapper .overlay>.svg-inline--fa,.small-box .overlay>.fa,.small-box .overlay>.fab,.small-box .overlay>.fad,.small-box .overlay>.fal,.small-box .overlay>.far,.small-box .overlay>.fas,.small-box .overlay>.ion,.small-box .overlay>.svg-inline--fa{color:#343a40}.card .overlay.dark,.info-box .overlay.dark,.overlay-wrapper .overlay.dark,.small-box .overlay.dark{background-color:rgba(0,0,0,.5)}.card .overlay.dark>.fa,.card .overlay.dark>.fab,.card .overlay.dark>.fad,.card .overlay.dark>.fal,.card .overlay.dark>.far,.card .overlay.dark>.fas,.card .overlay.dark>.ion,.card .overlay.dark>.svg-inline--fa,.info-box .overlay.dark>.fa,.info-box .overlay.dark>.fab,.info-box .overlay.dark>.fad,.info-box .overlay.dark>.fal,.info-box .overlay.dark>.far,.info-box .overlay.dark>.fas,.info-box .overlay.dark>.ion,.info-box .overlay.dark>.svg-inline--fa,.overlay-wrapper .overlay.dark>.fa,.overlay-wrapper .overlay.dark>.fab,.overlay-wrapper .overlay.dark>.fad,.overlay-wrapper .overlay.dark>.fal,.overlay-wrapper .overlay.dark>.far,.overlay-wrapper .overlay.dark>.fas,.overlay-wrapper .overlay.dark>.ion,.overlay-wrapper .overlay.dark>.svg-inline--fa,.small-box .overlay.dark>.fa,.small-box .overlay.dark>.fab,.small-box .overlay.dark>.fad,.small-box .overlay.dark>.fal,.small-box .overlay.dark>.far,.small-box .overlay.dark>.fas,.small-box .overlay.dark>.ion,.small-box .overlay.dark>.svg-inline--fa{color:#ced4da}.tab-pane>.overlay-wrapper{position:relative}.tab-pane>.overlay-wrapper>.overlay{border-top-left-radius:0;border-top-right-radius:0;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;margin-top:-1.25rem;margin-left:-1.25rem;height:calc(100% + 2 * 1.25rem);width:calc(100% + 2 * 1.25rem)}.tab-pane>.overlay-wrapper>.overlay.dark{color:#fff}.ribbon-wrapper{height:70px;overflow:hidden;position:absolute;right:-2px;top:-2px;width:70px;z-index:10}.ribbon-wrapper.ribbon-lg{height:120px;width:120px}.ribbon-wrapper.ribbon-lg .ribbon{right:0;top:26px;width:160px}.ribbon-wrapper.ribbon-xl{height:180px;width:180px}.ribbon-wrapper.ribbon-xl .ribbon{right:4px;top:47px;width:240px}.ribbon-wrapper .ribbon{box-shadow:0 0 3px rgba(0,0,0,.3);font-size:.8rem;line-height:100%;padding:.375rem 0;position:relative;right:-2px;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,.4);text-transform:uppercase;top:10px;-webkit-transform:rotate(45deg);transform:rotate(45deg);width:90px}.ribbon-wrapper .ribbon::after,.ribbon-wrapper .ribbon::before{border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #9e9e9e;bottom:-3px;content:"";position:absolute}.ribbon-wrapper .ribbon::before{left:0}.ribbon-wrapper .ribbon::after{right:0}.back-to-top{bottom:1.25rem;position:fixed;right:1.25rem;z-index:1032}.back-to-top:focus{box-shadow:none}pre{padding:.75rem}blockquote{background-color:#fff;border-left:.7rem solid #007bff;margin:1.5em .7rem;padding:.5em .7rem}.box blockquote{background-color:#e9ecef}blockquote p:last-child{margin-bottom:0}blockquote h1,blockquote h2,blockquote h3,blockquote h4,blockquote h5,blockquote h6{color:#007bff;font-size:1.25rem;font-weight:600}blockquote.quote-primary{border-color:#007bff}blockquote.quote-primary h1,blockquote.quote-primary h2,blockquote.quote-primary h3,blockquote.quote-primary h4,blockquote.quote-primary h5,blockquote.quote-primary h6{color:#007bff}blockquote.quote-secondary{border-color:#6c757d}blockquote.quote-secondary h1,blockquote.quote-secondary h2,blockquote.quote-secondary h3,blockquote.quote-secondary h4,blockquote.quote-secondary h5,blockquote.quote-secondary h6{color:#6c757d}blockquote.quote-success{border-color:#28a745}blockquote.quote-success h1,blockquote.quote-success h2,blockquote.quote-success h3,blockquote.quote-success h4,blockquote.quote-success h5,blockquote.quote-success h6{color:#28a745}blockquote.quote-info{border-color:#17a2b8}blockquote.quote-info h1,blockquote.quote-info h2,blockquote.quote-info h3,blockquote.quote-info h4,blockquote.quote-info h5,blockquote.quote-info h6{color:#17a2b8}blockquote.quote-warning{border-color:#ffc107}blockquote.quote-warning h1,blockquote.quote-warning h2,blockquote.quote-warning h3,blockquote.quote-warning h4,blockquote.quote-warning h5,blockquote.quote-warning h6{color:#ffc107}blockquote.quote-danger{border-color:#dc3545}blockquote.quote-danger h1,blockquote.quote-danger h2,blockquote.quote-danger h3,blockquote.quote-danger h4,blockquote.quote-danger h5,blockquote.quote-danger h6{color:#dc3545}blockquote.quote-light{border-color:#f8f9fa}blockquote.quote-light h1,blockquote.quote-light h2,blockquote.quote-light h3,blockquote.quote-light h4,blockquote.quote-light h5,blockquote.quote-light h6{color:#f8f9fa}blockquote.quote-dark{border-color:#343a40}blockquote.quote-dark h1,blockquote.quote-dark h2,blockquote.quote-dark h3,blockquote.quote-dark h4,blockquote.quote-dark h5,blockquote.quote-dark h6{color:#343a40}blockquote.quote-lightblue{border-color:#3c8dbc}blockquote.quote-lightblue h1,blockquote.quote-lightblue h2,blockquote.quote-lightblue h3,blockquote.quote-lightblue h4,blockquote.quote-lightblue h5,blockquote.quote-lightblue h6{color:#3c8dbc}blockquote.quote-navy{border-color:#001f3f}blockquote.quote-navy h1,blockquote.quote-navy h2,blockquote.quote-navy h3,blockquote.quote-navy h4,blockquote.quote-navy h5,blockquote.quote-navy h6{color:#001f3f}blockquote.quote-olive{border-color:#3d9970}blockquote.quote-olive h1,blockquote.quote-olive h2,blockquote.quote-olive h3,blockquote.quote-olive h4,blockquote.quote-olive h5,blockquote.quote-olive h6{color:#3d9970}blockquote.quote-lime{border-color:#01ff70}blockquote.quote-lime h1,blockquote.quote-lime h2,blockquote.quote-lime h3,blockquote.quote-lime h4,blockquote.quote-lime h5,blockquote.quote-lime h6{color:#01ff70}blockquote.quote-fuchsia{border-color:#f012be}blockquote.quote-fuchsia h1,blockquote.quote-fuchsia h2,blockquote.quote-fuchsia h3,blockquote.quote-fuchsia h4,blockquote.quote-fuchsia h5,blockquote.quote-fuchsia h6{color:#f012be}blockquote.quote-maroon{border-color:#d81b60}blockquote.quote-maroon h1,blockquote.quote-maroon h2,blockquote.quote-maroon h3,blockquote.quote-maroon h4,blockquote.quote-maroon h5,blockquote.quote-maroon h6{color:#d81b60}blockquote.quote-blue{border-color:#007bff}blockquote.quote-blue h1,blockquote.quote-blue h2,blockquote.quote-blue h3,blockquote.quote-blue h4,blockquote.quote-blue h5,blockquote.quote-blue h6{color:#007bff}blockquote.quote-indigo{border-color:#6610f2}blockquote.quote-indigo h1,blockquote.quote-indigo h2,blockquote.quote-indigo h3,blockquote.quote-indigo h4,blockquote.quote-indigo h5,blockquote.quote-indigo h6{color:#6610f2}blockquote.quote-purple{border-color:#6f42c1}blockquote.quote-purple h1,blockquote.quote-purple h2,blockquote.quote-purple h3,blockquote.quote-purple h4,blockquote.quote-purple h5,blockquote.quote-purple h6{color:#6f42c1}blockquote.quote-pink{border-color:#e83e8c}blockquote.quote-pink h1,blockquote.quote-pink h2,blockquote.quote-pink h3,blockquote.quote-pink h4,blockquote.quote-pink h5,blockquote.quote-pink h6{color:#e83e8c}blockquote.quote-red{border-color:#dc3545}blockquote.quote-red h1,blockquote.quote-red h2,blockquote.quote-red h3,blockquote.quote-red h4,blockquote.quote-red h5,blockquote.quote-red h6{color:#dc3545}blockquote.quote-orange{border-color:#fd7e14}blockquote.quote-orange h1,blockquote.quote-orange h2,blockquote.quote-orange h3,blockquote.quote-orange h4,blockquote.quote-orange h5,blockquote.quote-orange h6{color:#fd7e14}blockquote.quote-yellow{border-color:#ffc107}blockquote.quote-yellow h1,blockquote.quote-yellow h2,blockquote.quote-yellow h3,blockquote.quote-yellow h4,blockquote.quote-yellow h5,blockquote.quote-yellow h6{color:#ffc107}blockquote.quote-green{border-color:#28a745}blockquote.quote-green h1,blockquote.quote-green h2,blockquote.quote-green h3,blockquote.quote-green h4,blockquote.quote-green h5,blockquote.quote-green h6{color:#28a745}blockquote.quote-teal{border-color:#20c997}blockquote.quote-teal h1,blockquote.quote-teal h2,blockquote.quote-teal h3,blockquote.quote-teal h4,blockquote.quote-teal h5,blockquote.quote-teal h6{color:#20c997}blockquote.quote-cyan{border-color:#17a2b8}blockquote.quote-cyan h1,blockquote.quote-cyan h2,blockquote.quote-cyan h3,blockquote.quote-cyan h4,blockquote.quote-cyan h5,blockquote.quote-cyan h6{color:#17a2b8}blockquote.quote-white{border-color:#fff}blockquote.quote-white h1,blockquote.quote-white h2,blockquote.quote-white h3,blockquote.quote-white h4,blockquote.quote-white h5,blockquote.quote-white h6{color:#fff}blockquote.quote-gray{border-color:#6c757d}blockquote.quote-gray h1,blockquote.quote-gray h2,blockquote.quote-gray h3,blockquote.quote-gray h4,blockquote.quote-gray h5,blockquote.quote-gray h6{color:#6c757d}blockquote.quote-gray-dark{border-color:#343a40}blockquote.quote-gray-dark h1,blockquote.quote-gray-dark h2,blockquote.quote-gray-dark h3,blockquote.quote-gray-dark h4,blockquote.quote-gray-dark h5,blockquote.quote-gray-dark h6{color:#343a40}.tab-custom-content{border-top:1px solid #dee2e6;margin-top:.5rem;padding-top:.5rem}.nav+.tab-custom-content{border-top:none;border-bottom:1px solid #dee2e6;margin-top:0;margin-bottom:.5rem;padding-bottom:.5rem}.badge-btn{border-radius:.15rem;font-size:.75rem;font-weight:400;padding:.25rem .5rem}.badge-btn.badge-pill{padding:.375rem .6rem}.dark-mode a:not(.btn):hover{color:#3395ff}.dark-mode .attachment-block{background-color:#3d444b}.dark-mode .attachment-block .attachment-text{color:#ced4da}.dark-mode blockquote{background-color:#3f474e}.dark-mode blockquote.quote-primary{border-color:#007bff}.dark-mode blockquote.quote-primary h1,.dark-mode blockquote.quote-primary h2,.dark-mode blockquote.quote-primary h3,.dark-mode blockquote.quote-primary h4,.dark-mode blockquote.quote-primary h5,.dark-mode blockquote.quote-primary h6{color:#007bff}.dark-mode blockquote.quote-secondary{border-color:#6c757d}.dark-mode blockquote.quote-secondary h1,.dark-mode blockquote.quote-secondary h2,.dark-mode blockquote.quote-secondary h3,.dark-mode blockquote.quote-secondary h4,.dark-mode blockquote.quote-secondary h5,.dark-mode blockquote.quote-secondary h6{color:#6c757d}.dark-mode blockquote.quote-success{border-color:#28a745}.dark-mode blockquote.quote-success h1,.dark-mode blockquote.quote-success h2,.dark-mode blockquote.quote-success h3,.dark-mode blockquote.quote-success h4,.dark-mode blockquote.quote-success h5,.dark-mode blockquote.quote-success h6{color:#28a745}.dark-mode blockquote.quote-info{border-color:#17a2b8}.dark-mode blockquote.quote-info h1,.dark-mode blockquote.quote-info h2,.dark-mode blockquote.quote-info h3,.dark-mode blockquote.quote-info h4,.dark-mode blockquote.quote-info h5,.dark-mode blockquote.quote-info h6{color:#17a2b8}.dark-mode blockquote.quote-warning{border-color:#ffc107}.dark-mode blockquote.quote-warning h1,.dark-mode blockquote.quote-warning h2,.dark-mode blockquote.quote-warning h3,.dark-mode blockquote.quote-warning h4,.dark-mode blockquote.quote-warning h5,.dark-mode blockquote.quote-warning h6{color:#ffc107}.dark-mode blockquote.quote-danger{border-color:#dc3545}.dark-mode blockquote.quote-danger h1,.dark-mode blockquote.quote-danger h2,.dark-mode blockquote.quote-danger h3,.dark-mode blockquote.quote-danger h4,.dark-mode blockquote.quote-danger h5,.dark-mode blockquote.quote-danger h6{color:#dc3545}.dark-mode blockquote.quote-light{border-color:#f8f9fa}.dark-mode blockquote.quote-light h1,.dark-mode blockquote.quote-light h2,.dark-mode blockquote.quote-light h3,.dark-mode blockquote.quote-light h4,.dark-mode blockquote.quote-light h5,.dark-mode blockquote.quote-light h6{color:#f8f9fa}.dark-mode blockquote.quote-dark{border-color:#343a40}.dark-mode blockquote.quote-dark h1,.dark-mode blockquote.quote-dark h2,.dark-mode blockquote.quote-dark h3,.dark-mode blockquote.quote-dark h4,.dark-mode blockquote.quote-dark h5,.dark-mode blockquote.quote-dark h6{color:#343a40}.dark-mode blockquote.quote-lightblue{border-color:#3c8dbc}.dark-mode blockquote.quote-lightblue h1,.dark-mode blockquote.quote-lightblue h2,.dark-mode blockquote.quote-lightblue h3,.dark-mode blockquote.quote-lightblue h4,.dark-mode blockquote.quote-lightblue h5,.dark-mode blockquote.quote-lightblue h6{color:#3c8dbc}.dark-mode blockquote.quote-navy{border-color:#001f3f}.dark-mode blockquote.quote-navy h1,.dark-mode blockquote.quote-navy h2,.dark-mode blockquote.quote-navy h3,.dark-mode blockquote.quote-navy h4,.dark-mode blockquote.quote-navy h5,.dark-mode blockquote.quote-navy h6{color:#001f3f}.dark-mode blockquote.quote-olive{border-color:#3d9970}.dark-mode blockquote.quote-olive h1,.dark-mode blockquote.quote-olive h2,.dark-mode blockquote.quote-olive h3,.dark-mode blockquote.quote-olive h4,.dark-mode blockquote.quote-olive h5,.dark-mode blockquote.quote-olive h6{color:#3d9970}.dark-mode blockquote.quote-lime{border-color:#01ff70}.dark-mode blockquote.quote-lime h1,.dark-mode blockquote.quote-lime h2,.dark-mode blockquote.quote-lime h3,.dark-mode blockquote.quote-lime h4,.dark-mode blockquote.quote-lime h5,.dark-mode blockquote.quote-lime h6{color:#01ff70}.dark-mode blockquote.quote-fuchsia{border-color:#f012be}.dark-mode blockquote.quote-fuchsia h1,.dark-mode blockquote.quote-fuchsia h2,.dark-mode blockquote.quote-fuchsia h3,.dark-mode blockquote.quote-fuchsia h4,.dark-mode blockquote.quote-fuchsia h5,.dark-mode blockquote.quote-fuchsia h6{color:#f012be}.dark-mode blockquote.quote-maroon{border-color:#d81b60}.dark-mode blockquote.quote-maroon h1,.dark-mode blockquote.quote-maroon h2,.dark-mode blockquote.quote-maroon h3,.dark-mode blockquote.quote-maroon h4,.dark-mode blockquote.quote-maroon h5,.dark-mode blockquote.quote-maroon h6{color:#d81b60}.dark-mode blockquote.quote-blue{border-color:#007bff}.dark-mode blockquote.quote-blue h1,.dark-mode blockquote.quote-blue h2,.dark-mode blockquote.quote-blue h3,.dark-mode blockquote.quote-blue h4,.dark-mode blockquote.quote-blue h5,.dark-mode blockquote.quote-blue h6{color:#007bff}.dark-mode blockquote.quote-indigo{border-color:#6610f2}.dark-mode blockquote.quote-indigo h1,.dark-mode blockquote.quote-indigo h2,.dark-mode blockquote.quote-indigo h3,.dark-mode blockquote.quote-indigo h4,.dark-mode blockquote.quote-indigo h5,.dark-mode blockquote.quote-indigo h6{color:#6610f2}.dark-mode blockquote.quote-purple{border-color:#6f42c1}.dark-mode blockquote.quote-purple h1,.dark-mode blockquote.quote-purple h2,.dark-mode blockquote.quote-purple h3,.dark-mode blockquote.quote-purple h4,.dark-mode blockquote.quote-purple h5,.dark-mode blockquote.quote-purple h6{color:#6f42c1}.dark-mode blockquote.quote-pink{border-color:#e83e8c}.dark-mode blockquote.quote-pink h1,.dark-mode blockquote.quote-pink h2,.dark-mode blockquote.quote-pink h3,.dark-mode blockquote.quote-pink h4,.dark-mode blockquote.quote-pink h5,.dark-mode blockquote.quote-pink h6{color:#e83e8c}.dark-mode blockquote.quote-red{border-color:#dc3545}.dark-mode blockquote.quote-red h1,.dark-mode blockquote.quote-red h2,.dark-mode blockquote.quote-red h3,.dark-mode blockquote.quote-red h4,.dark-mode blockquote.quote-red h5,.dark-mode blockquote.quote-red h6{color:#dc3545}.dark-mode blockquote.quote-orange{border-color:#fd7e14}.dark-mode blockquote.quote-orange h1,.dark-mode blockquote.quote-orange h2,.dark-mode blockquote.quote-orange h3,.dark-mode blockquote.quote-orange h4,.dark-mode blockquote.quote-orange h5,.dark-mode blockquote.quote-orange h6{color:#fd7e14}.dark-mode blockquote.quote-yellow{border-color:#ffc107}.dark-mode blockquote.quote-yellow h1,.dark-mode blockquote.quote-yellow h2,.dark-mode blockquote.quote-yellow h3,.dark-mode blockquote.quote-yellow h4,.dark-mode blockquote.quote-yellow h5,.dark-mode blockquote.quote-yellow h6{color:#ffc107}.dark-mode blockquote.quote-green{border-color:#28a745}.dark-mode blockquote.quote-green h1,.dark-mode blockquote.quote-green h2,.dark-mode blockquote.quote-green h3,.dark-mode blockquote.quote-green h4,.dark-mode blockquote.quote-green h5,.dark-mode blockquote.quote-green h6{color:#28a745}.dark-mode blockquote.quote-teal{border-color:#20c997}.dark-mode blockquote.quote-teal h1,.dark-mode blockquote.quote-teal h2,.dark-mode blockquote.quote-teal h3,.dark-mode blockquote.quote-teal h4,.dark-mode blockquote.quote-teal h5,.dark-mode blockquote.quote-teal h6{color:#20c997}.dark-mode blockquote.quote-cyan{border-color:#17a2b8}.dark-mode blockquote.quote-cyan h1,.dark-mode blockquote.quote-cyan h2,.dark-mode blockquote.quote-cyan h3,.dark-mode blockquote.quote-cyan h4,.dark-mode blockquote.quote-cyan h5,.dark-mode blockquote.quote-cyan h6{color:#17a2b8}.dark-mode blockquote.quote-white{border-color:#fff}.dark-mode blockquote.quote-white h1,.dark-mode blockquote.quote-white h2,.dark-mode blockquote.quote-white h3,.dark-mode blockquote.quote-white h4,.dark-mode blockquote.quote-white h5,.dark-mode blockquote.quote-white h6{color:#fff}.dark-mode blockquote.quote-gray{border-color:#6c757d}.dark-mode blockquote.quote-gray h1,.dark-mode blockquote.quote-gray h2,.dark-mode blockquote.quote-gray h3,.dark-mode blockquote.quote-gray h4,.dark-mode blockquote.quote-gray h5,.dark-mode blockquote.quote-gray h6{color:#6c757d}.dark-mode blockquote.quote-gray-dark{border-color:#343a40}.dark-mode blockquote.quote-gray-dark h1,.dark-mode blockquote.quote-gray-dark h2,.dark-mode blockquote.quote-gray-dark h3,.dark-mode blockquote.quote-gray-dark h4,.dark-mode blockquote.quote-gray-dark h5,.dark-mode blockquote.quote-gray-dark h6{color:#343a40}.dark-mode .close,.dark-mode .mailbox-attachment-close{color:#adb5bd;text-shadow:0 1px 0 #495057}.dark-mode .tab-custom-content{border-color:#6c757d}.dark-mode .list-group-item{background-color:#343a40;border-color:#6c757d}@media print{.content-header,.main-header,.main-sidebar,.no-print{display:none!important}.content-wrapper,.main-footer{-webkit-transform:translate(0,0);transform:translate(0,0);margin-left:0!important;min-height:0!important}.layout-fixed .content-wrapper{padding-top:0!important}.invoice{border:0;margin:0;padding:0;width:100%}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr td,.table-responsive>.table tr th{white-space:normal!important}}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-xs{font-size:.75rem!important}.text-sm{font-size:.875rem!important}.text-md{font-size:1rem!important}.text-lg{font-size:1.25rem!important}.text-xl{font-size:2rem!important}.text-lightblue{color:#3c8dbc!important}.text-navy{color:#001f3f!important}.text-olive{color:#3d9970!important}.text-lime{color:#01ff70!important}.text-fuchsia{color:#f012be!important}.text-maroon{color:#d81b60!important}.text-blue{color:#007bff!important}.text-indigo{color:#6610f2!important}.text-purple{color:#6f42c1!important}.text-pink{color:#e83e8c!important}.text-red{color:#dc3545!important}.text-orange{color:#fd7e14!important}.text-yellow{color:#ffc107!important}.text-green{color:#28a745!important}.text-teal{color:#20c997!important}.text-cyan{color:#17a2b8!important}.text-white{color:#fff!important}.text-gray{color:#6c757d!important}.text-gray-dark{color:#343a40!important}.dark-mode .text-muted{color:#adb5bd!important}.dark-mode .text-lightblue{color:#86bad8!important}.dark-mode .text-navy{color:#002c59!important}.dark-mode .text-olive{color:#74c8a3!important}.dark-mode .text-lime{color:#67ffa9!important}.dark-mode .text-fuchsia{color:#f672d8!important}.dark-mode .text-maroon{color:#ed6c9b!important}.dark-mode .text-blue{color:#3f6791!important}.dark-mode .text-indigo{color:#6610f2!important}.dark-mode .text-purple{color:#6f42c1!important}.dark-mode .text-pink{color:#e83e8c!important}.dark-mode .text-red{color:#e74c3c!important}.dark-mode .text-orange{color:#fd7e14!important}.dark-mode .text-yellow{color:#f39c12!important}.dark-mode .text-green{color:#00bc8c!important}.dark-mode .text-teal{color:#20c997!important}.dark-mode .text-cyan{color:#3498db!important}.dark-mode .text-white{color:#fff!important}.dark-mode .text-gray{color:#6c757d!important}.dark-mode .text-gray-dark{color:#343a40!important}.elevation-0{box-shadow:none!important}.elevation-1{box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)!important}.elevation-2{box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)!important}.elevation-3{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)!important}.elevation-4{box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22)!important}.elevation-5{box-shadow:0 19px 38px rgba(0,0,0,.3),0 15px 12px rgba(0,0,0,.22)!important}.bg-primary{background-color:#007bff!important}.bg-primary,.bg-primary>a{color:#fff!important}.bg-primary.btn:hover{border-color:#0062cc;color:#ececec}.bg-primary.btn.active,.bg-primary.btn:active,.bg-primary.btn:not(:disabled):not(.disabled).active,.bg-primary.btn:not(:disabled):not(.disabled):active{background-color:#0062cc!important;border-color:#005cbf;color:#fff}.bg-secondary{background-color:#6c757d!important}.bg-secondary,.bg-secondary>a{color:#fff!important}.bg-secondary.btn:hover{border-color:#545b62;color:#ececec}.bg-secondary.btn.active,.bg-secondary.btn:active,.bg-secondary.btn:not(:disabled):not(.disabled).active,.bg-secondary.btn:not(:disabled):not(.disabled):active{background-color:#545b62!important;border-color:#4e555b;color:#fff}.bg-success{background-color:#28a745!important}.bg-success,.bg-success>a{color:#fff!important}.bg-success.btn:hover{border-color:#1e7e34;color:#ececec}.bg-success.btn.active,.bg-success.btn:active,.bg-success.btn:not(:disabled):not(.disabled).active,.bg-success.btn:not(:disabled):not(.disabled):active{background-color:#1e7e34!important;border-color:#1c7430;color:#fff}.bg-info{background-color:#17a2b8!important}.bg-info,.bg-info>a{color:#fff!important}.bg-info.btn:hover{border-color:#117a8b;color:#ececec}.bg-info.btn.active,.bg-info.btn:active,.bg-info.btn:not(:disabled):not(.disabled).active,.bg-info.btn:not(:disabled):not(.disabled):active{background-color:#117a8b!important;border-color:#10707f;color:#fff}.bg-warning{background-color:#ffc107!important}.bg-warning,.bg-warning>a{color:#1f2d3d!important}.bg-warning.btn:hover{border-color:#d39e00;color:#121a24}.bg-warning.btn.active,.bg-warning.btn:active,.bg-warning.btn:not(:disabled):not(.disabled).active,.bg-warning.btn:not(:disabled):not(.disabled):active{background-color:#d39e00!important;border-color:#c69500;color:#1f2d3d}.bg-danger{background-color:#dc3545!important}.bg-danger,.bg-danger>a{color:#fff!important}.bg-danger.btn:hover{border-color:#bd2130;color:#ececec}.bg-danger.btn.active,.bg-danger.btn:active,.bg-danger.btn:not(:disabled):not(.disabled).active,.bg-danger.btn:not(:disabled):not(.disabled):active{background-color:#bd2130!important;border-color:#b21f2d;color:#fff}.bg-light{background-color:#f8f9fa!important}.bg-light,.bg-light>a{color:#1f2d3d!important}.bg-light.btn:hover{border-color:#dae0e5;color:#121a24}.bg-light.btn.active,.bg-light.btn:active,.bg-light.btn:not(:disabled):not(.disabled).active,.bg-light.btn:not(:disabled):not(.disabled):active{background-color:#dae0e5!important;border-color:#d3d9df;color:#1f2d3d}.bg-dark{background-color:#343a40!important}.bg-dark,.bg-dark>a{color:#fff!important}.bg-dark.btn:hover{border-color:#1d2124;color:#ececec}.bg-dark.btn.active,.bg-dark.btn:active,.bg-dark.btn:not(:disabled):not(.disabled).active,.bg-dark.btn:not(:disabled):not(.disabled):active{background-color:#1d2124!important;border-color:#171a1d;color:#fff}.bg-lightblue{background-color:#3c8dbc!important}.bg-lightblue,.bg-lightblue>a{color:#fff!important}.bg-lightblue.btn:hover{border-color:#307095;color:#ececec}.bg-lightblue.btn.active,.bg-lightblue.btn:active,.bg-lightblue.btn:not(:disabled):not(.disabled).active,.bg-lightblue.btn:not(:disabled):not(.disabled):active{background-color:#307095!important;border-color:#2d698c;color:#fff}.bg-navy{background-color:#001f3f!important}.bg-navy,.bg-navy>a{color:#fff!important}.bg-navy.btn:hover{border-color:#00060c;color:#ececec}.bg-navy.btn.active,.bg-navy.btn:active,.bg-navy.btn:not(:disabled):not(.disabled).active,.bg-navy.btn:not(:disabled):not(.disabled):active{background-color:#00060c!important;border-color:#000;color:#fff}.bg-olive{background-color:#3d9970!important}.bg-olive,.bg-olive>a{color:#fff!important}.bg-olive.btn:hover{border-color:#2e7555;color:#ececec}.bg-olive.btn.active,.bg-olive.btn:active,.bg-olive.btn:not(:disabled):not(.disabled).active,.bg-olive.btn:not(:disabled):not(.disabled):active{background-color:#2e7555!important;border-color:#2b6b4f;color:#fff}.bg-lime{background-color:#01ff70!important}.bg-lime,.bg-lime>a{color:#1f2d3d!important}.bg-lime.btn:hover{border-color:#00cd5a;color:#121a24}.bg-lime.btn.active,.bg-lime.btn:active,.bg-lime.btn:not(:disabled):not(.disabled).active,.bg-lime.btn:not(:disabled):not(.disabled):active{background-color:#00cd5a!important;border-color:#00c054;color:#fff}.bg-fuchsia{background-color:#f012be!important}.bg-fuchsia,.bg-fuchsia>a{color:#fff!important}.bg-fuchsia.btn:hover{border-color:#c30c9a;color:#ececec}.bg-fuchsia.btn.active,.bg-fuchsia.btn:active,.bg-fuchsia.btn:not(:disabled):not(.disabled).active,.bg-fuchsia.btn:not(:disabled):not(.disabled):active{background-color:#c30c9a!important;border-color:#b70c90;color:#fff}.bg-maroon{background-color:#d81b60!important}.bg-maroon,.bg-maroon>a{color:#fff!important}.bg-maroon.btn:hover{border-color:#ab154c;color:#ececec}.bg-maroon.btn.active,.bg-maroon.btn:active,.bg-maroon.btn:not(:disabled):not(.disabled).active,.bg-maroon.btn:not(:disabled):not(.disabled):active{background-color:#ab154c!important;border-color:#9f1447;color:#fff}.bg-blue{background-color:#007bff!important}.bg-blue,.bg-blue>a{color:#fff!important}.bg-blue.btn:hover{border-color:#0062cc;color:#ececec}.bg-blue.btn.active,.bg-blue.btn:active,.bg-blue.btn:not(:disabled):not(.disabled).active,.bg-blue.btn:not(:disabled):not(.disabled):active{background-color:#0062cc!important;border-color:#005cbf;color:#fff}.bg-indigo{background-color:#6610f2!important}.bg-indigo,.bg-indigo>a{color:#fff!important}.bg-indigo.btn:hover{border-color:#510bc4;color:#ececec}.bg-indigo.btn.active,.bg-indigo.btn:active,.bg-indigo.btn:not(:disabled):not(.disabled).active,.bg-indigo.btn:not(:disabled):not(.disabled):active{background-color:#510bc4!important;border-color:#4c0ab8;color:#fff}.bg-purple{background-color:#6f42c1!important}.bg-purple,.bg-purple>a{color:#fff!important}.bg-purple.btn:hover{border-color:#59339d;color:#ececec}.bg-purple.btn.active,.bg-purple.btn:active,.bg-purple.btn:not(:disabled):not(.disabled).active,.bg-purple.btn:not(:disabled):not(.disabled):active{background-color:#59339d!important;border-color:#533093;color:#fff}.bg-pink{background-color:#e83e8c!important}.bg-pink,.bg-pink>a{color:#fff!important}.bg-pink.btn:hover{border-color:#d91a72;color:#ececec}.bg-pink.btn.active,.bg-pink.btn:active,.bg-pink.btn:not(:disabled):not(.disabled).active,.bg-pink.btn:not(:disabled):not(.disabled):active{background-color:#d91a72!important;border-color:#ce196c;color:#fff}.bg-red{background-color:#dc3545!important}.bg-red,.bg-red>a{color:#fff!important}.bg-red.btn:hover{border-color:#bd2130;color:#ececec}.bg-red.btn.active,.bg-red.btn:active,.bg-red.btn:not(:disabled):not(.disabled).active,.bg-red.btn:not(:disabled):not(.disabled):active{background-color:#bd2130!important;border-color:#b21f2d;color:#fff}.bg-orange{background-color:#fd7e14!important}.bg-orange,.bg-orange>a{color:#1f2d3d!important}.bg-orange.btn:hover{border-color:#dc6502;color:#121a24}.bg-orange.btn.active,.bg-orange.btn:active,.bg-orange.btn:not(:disabled):not(.disabled).active,.bg-orange.btn:not(:disabled):not(.disabled):active{background-color:#dc6502!important;border-color:#cf5f02;color:#fff}.bg-yellow{background-color:#ffc107!important}.bg-yellow,.bg-yellow>a{color:#1f2d3d!important}.bg-yellow.btn:hover{border-color:#d39e00;color:#121a24}.bg-yellow.btn.active,.bg-yellow.btn:active,.bg-yellow.btn:not(:disabled):not(.disabled).active,.bg-yellow.btn:not(:disabled):not(.disabled):active{background-color:#d39e00!important;border-color:#c69500;color:#1f2d3d}.bg-green{background-color:#28a745!important}.bg-green,.bg-green>a{color:#fff!important}.bg-green.btn:hover{border-color:#1e7e34;color:#ececec}.bg-green.btn.active,.bg-green.btn:active,.bg-green.btn:not(:disabled):not(.disabled).active,.bg-green.btn:not(:disabled):not(.disabled):active{background-color:#1e7e34!important;border-color:#1c7430;color:#fff}.bg-teal{background-color:#20c997!important}.bg-teal,.bg-teal>a{color:#fff!important}.bg-teal.btn:hover{border-color:#199d76;color:#ececec}.bg-teal.btn.active,.bg-teal.btn:active,.bg-teal.btn:not(:disabled):not(.disabled).active,.bg-teal.btn:not(:disabled):not(.disabled):active{background-color:#199d76!important;border-color:#17926e;color:#fff}.bg-cyan{background-color:#17a2b8!important}.bg-cyan,.bg-cyan>a{color:#fff!important}.bg-cyan.btn:hover{border-color:#117a8b;color:#ececec}.bg-cyan.btn.active,.bg-cyan.btn:active,.bg-cyan.btn:not(:disabled):not(.disabled).active,.bg-cyan.btn:not(:disabled):not(.disabled):active{background-color:#117a8b!important;border-color:#10707f;color:#fff}.bg-white{background-color:#fff!important}.bg-white,.bg-white>a{color:#1f2d3d!important}.bg-white.btn:hover{border-color:#e6e6e6;color:#121a24}.bg-white.btn.active,.bg-white.btn:active,.bg-white.btn:not(:disabled):not(.disabled).active,.bg-white.btn:not(:disabled):not(.disabled):active{background-color:#e6e6e6!important;border-color:#dfdfdf;color:#1f2d3d}.bg-gray{background-color:#6c757d!important}.bg-gray,.bg-gray>a{color:#fff!important}.bg-gray.btn:hover{border-color:#545b62;color:#ececec}.bg-gray.btn.active,.bg-gray.btn:active,.bg-gray.btn:not(:disabled):not(.disabled).active,.bg-gray.btn:not(:disabled):not(.disabled):active{background-color:#545b62!important;border-color:#4e555b;color:#fff}.bg-gray-dark{background-color:#343a40!important}.bg-gray-dark,.bg-gray-dark>a{color:#fff!important}.bg-gray-dark.btn:hover{border-color:#1d2124;color:#ececec}.bg-gray-dark.btn.active,.bg-gray-dark.btn:active,.bg-gray-dark.btn:not(:disabled):not(.disabled).active,.bg-gray-dark.btn:not(:disabled):not(.disabled):active{background-color:#1d2124!important;border-color:#171a1d;color:#fff}.bg-gray{background-color:#adb5bd;color:#1f2d3d}.bg-gray-light{background-color:#f2f4f5;color:#1f2d3d!important}.bg-black{background-color:#000;color:#fff!important}.bg-white{background-color:#fff;color:#1f2d3d!important}.bg-gradient-primary{background:#007bff linear-gradient(180deg,#268fff,#007bff) repeat-x!important;color:#fff}.bg-gradient-primary.btn.disabled,.bg-gradient-primary.btn:disabled,.bg-gradient-primary.btn:not(:disabled):not(.disabled).active,.bg-gradient-primary.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-primary.btn.dropdown-toggle{background-image:none!important}.bg-gradient-primary.btn:hover{background:#007bff linear-gradient(180deg,#267fde,#0069d9) repeat-x!important;border-color:#0062cc;color:#ececec}.bg-gradient-primary.btn.active,.bg-gradient-primary.btn:active,.bg-gradient-primary.btn:not(:disabled):not(.disabled).active,.bg-gradient-primary.btn:not(:disabled):not(.disabled):active{background:#007bff linear-gradient(180deg,#267ad4,#0062cc) repeat-x!important;border-color:#005cbf;color:#fff}.bg-gradient-secondary{background:#6c757d linear-gradient(180deg,#828a91,#6c757d) repeat-x!important;color:#fff}.bg-gradient-secondary.btn.disabled,.bg-gradient-secondary.btn:disabled,.bg-gradient-secondary.btn:not(:disabled):not(.disabled).active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-secondary.btn.dropdown-toggle{background-image:none!important}.bg-gradient-secondary.btn:hover{background:#6c757d linear-gradient(180deg,#73797f,#5a6268) repeat-x!important;border-color:#545b62;color:#ececec}.bg-gradient-secondary.btn.active,.bg-gradient-secondary.btn:active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled).active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled):active{background:#6c757d linear-gradient(180deg,#6e7479,#545b62) repeat-x!important;border-color:#4e555b;color:#fff}.bg-gradient-success{background:#28a745 linear-gradient(180deg,#48b461,#28a745) repeat-x!important;color:#fff}.bg-gradient-success.btn.disabled,.bg-gradient-success.btn:disabled,.bg-gradient-success.btn:not(:disabled):not(.disabled).active,.bg-gradient-success.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-success.btn.dropdown-toggle{background-image:none!important}.bg-gradient-success.btn:hover{background:#28a745 linear-gradient(180deg,#429a56,#218838) repeat-x!important;border-color:#1e7e34;color:#ececec}.bg-gradient-success.btn.active,.bg-gradient-success.btn:active,.bg-gradient-success.btn:not(:disabled):not(.disabled).active,.bg-gradient-success.btn:not(:disabled):not(.disabled):active{background:#28a745 linear-gradient(180deg,#409152,#1e7e34) repeat-x!important;border-color:#1c7430;color:#fff}.bg-gradient-info{background:#17a2b8 linear-gradient(180deg,#3ab0c3,#17a2b8) repeat-x!important;color:#fff}.bg-gradient-info.btn.disabled,.bg-gradient-info.btn:disabled,.bg-gradient-info.btn:not(:disabled):not(.disabled).active,.bg-gradient-info.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-info.btn.dropdown-toggle{background-image:none!important}.bg-gradient-info.btn:hover{background:#17a2b8 linear-gradient(180deg,#3697a6,#138496) repeat-x!important;border-color:#117a8b;color:#ececec}.bg-gradient-info.btn.active,.bg-gradient-info.btn:active,.bg-gradient-info.btn:not(:disabled):not(.disabled).active,.bg-gradient-info.btn:not(:disabled):not(.disabled):active{background:#17a2b8 linear-gradient(180deg,#358e9c,#117a8b) repeat-x!important;border-color:#10707f;color:#fff}.bg-gradient-warning{background:#ffc107 linear-gradient(180deg,#ffca2c,#ffc107) repeat-x!important;color:#1f2d3d}.bg-gradient-warning.btn.disabled,.bg-gradient-warning.btn:disabled,.bg-gradient-warning.btn:not(:disabled):not(.disabled).active,.bg-gradient-warning.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-warning.btn.dropdown-toggle{background-image:none!important}.bg-gradient-warning.btn:hover{background:#ffc107 linear-gradient(180deg,#e4b526,#e0a800) repeat-x!important;border-color:#d39e00;color:#121a24}.bg-gradient-warning.btn.active,.bg-gradient-warning.btn:active,.bg-gradient-warning.btn:not(:disabled):not(.disabled).active,.bg-gradient-warning.btn:not(:disabled):not(.disabled):active{background:#ffc107 linear-gradient(180deg,#daad26,#d39e00) repeat-x!important;border-color:#c69500;color:#1f2d3d}.bg-gradient-danger{background:#dc3545 linear-gradient(180deg,#e15361,#dc3545) repeat-x!important;color:#fff}.bg-gradient-danger.btn.disabled,.bg-gradient-danger.btn:disabled,.bg-gradient-danger.btn:not(:disabled):not(.disabled).active,.bg-gradient-danger.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-danger.btn.dropdown-toggle{background-image:none!important}.bg-gradient-danger.btn:hover{background:#dc3545 linear-gradient(180deg,#d04451,#c82333) repeat-x!important;border-color:#bd2130;color:#ececec}.bg-gradient-danger.btn.active,.bg-gradient-danger.btn:active,.bg-gradient-danger.btn:not(:disabled):not(.disabled).active,.bg-gradient-danger.btn:not(:disabled):not(.disabled):active{background:#dc3545 linear-gradient(180deg,#c7424f,#bd2130) repeat-x!important;border-color:#b21f2d;color:#fff}.bg-gradient-light{background:#f8f9fa linear-gradient(180deg,#f9fafb,#f8f9fa) repeat-x!important;color:#1f2d3d}.bg-gradient-light.btn.disabled,.bg-gradient-light.btn:disabled,.bg-gradient-light.btn:not(:disabled):not(.disabled).active,.bg-gradient-light.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-light.btn.dropdown-toggle{background-image:none!important}.bg-gradient-light.btn:hover{background:#f8f9fa linear-gradient(180deg,#e6eaed,#e2e6ea) repeat-x!important;border-color:#dae0e5;color:#121a24}.bg-gradient-light.btn.active,.bg-gradient-light.btn:active,.bg-gradient-light.btn:not(:disabled):not(.disabled).active,.bg-gradient-light.btn:not(:disabled):not(.disabled):active{background:#f8f9fa linear-gradient(180deg,#e0e4e9,#dae0e5) repeat-x!important;border-color:#d3d9df;color:#1f2d3d}.bg-gradient-dark{background:#343a40 linear-gradient(180deg,#52585d,#343a40) repeat-x!important;color:#fff}.bg-gradient-dark.btn.disabled,.bg-gradient-dark.btn:disabled,.bg-gradient-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-dark.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-dark.btn.dropdown-toggle{background-image:none!important}.bg-gradient-dark.btn:hover{background:#343a40 linear-gradient(180deg,#44474b,#23272b) repeat-x!important;border-color:#1d2124;color:#ececec}.bg-gradient-dark.btn.active,.bg-gradient-dark.btn:active,.bg-gradient-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-dark.btn:not(:disabled):not(.disabled):active{background:#343a40 linear-gradient(180deg,#3f4245,#1d2124) repeat-x!important;border-color:#171a1d;color:#fff}.bg-gradient-lightblue{background:#3c8dbc linear-gradient(180deg,#599ec6,#3c8dbc) repeat-x!important;color:#fff}.bg-gradient-lightblue.btn.disabled,.bg-gradient-lightblue.btn:disabled,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled).active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-lightblue.btn.dropdown-toggle{background-image:none!important}.bg-gradient-lightblue.btn:hover{background:#3c8dbc linear-gradient(180deg,#518cad,#33779f) repeat-x!important;border-color:#307095;color:#ececec}.bg-gradient-lightblue.btn.active,.bg-gradient-lightblue.btn:active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled).active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled):active{background:#3c8dbc linear-gradient(180deg,#4f85a5,#307095) repeat-x!important;border-color:#2d698c;color:#fff}.bg-gradient-navy{background:#001f3f linear-gradient(180deg,#26415c,#001f3f) repeat-x!important;color:#fff}.bg-gradient-navy.btn.disabled,.bg-gradient-navy.btn:disabled,.bg-gradient-navy.btn:not(:disabled):not(.disabled).active,.bg-gradient-navy.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-navy.btn.dropdown-toggle{background-image:none!important}.bg-gradient-navy.btn:hover{background:#001f3f linear-gradient(180deg,#26313b,#000c19) repeat-x!important;border-color:#00060c;color:#ececec}.bg-gradient-navy.btn.active,.bg-gradient-navy.btn:active,.bg-gradient-navy.btn:not(:disabled):not(.disabled).active,.bg-gradient-navy.btn:not(:disabled):not(.disabled):active{background:#001f3f linear-gradient(180deg,#262b30,#00060c) repeat-x!important;border-color:#000;color:#fff}.bg-gradient-olive{background:#3d9970 linear-gradient(180deg,#5aa885,#3d9970) repeat-x!important;color:#fff}.bg-gradient-olive.btn.disabled,.bg-gradient-olive.btn:disabled,.bg-gradient-olive.btn:not(:disabled):not(.disabled).active,.bg-gradient-olive.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-olive.btn.dropdown-toggle{background-image:none!important}.bg-gradient-olive.btn:hover{background:#3d9970 linear-gradient(180deg,#519174,#327e5c) repeat-x!important;border-color:#2e7555;color:#ececec}.bg-gradient-olive.btn.active,.bg-gradient-olive.btn:active,.bg-gradient-olive.btn:not(:disabled):not(.disabled).active,.bg-gradient-olive.btn:not(:disabled):not(.disabled):active{background:#3d9970 linear-gradient(180deg,#4e896f,#2e7555) repeat-x!important;border-color:#2b6b4f;color:#fff}.bg-gradient-lime{background:#01ff70 linear-gradient(180deg,#27ff85,#01ff70) repeat-x!important;color:#1f2d3d}.bg-gradient-lime.btn.disabled,.bg-gradient-lime.btn:disabled,.bg-gradient-lime.btn:not(:disabled):not(.disabled).active,.bg-gradient-lime.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-lime.btn.dropdown-toggle{background-image:none!important}.bg-gradient-lime.btn:hover{background:#01ff70 linear-gradient(180deg,#26df77,#00da5f) repeat-x!important;border-color:#00cd5a;color:#121a24}.bg-gradient-lime.btn.active,.bg-gradient-lime.btn:active,.bg-gradient-lime.btn:not(:disabled):not(.disabled).active,.bg-gradient-lime.btn:not(:disabled):not(.disabled):active{background:#01ff70 linear-gradient(180deg,#26d572,#00cd5a) repeat-x!important;border-color:#00c054;color:#fff}.bg-gradient-fuchsia{background:#f012be linear-gradient(180deg,#f236c8,#f012be) repeat-x!important;color:#fff}.bg-gradient-fuchsia.btn.disabled,.bg-gradient-fuchsia.btn:disabled,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled).active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-fuchsia.btn.dropdown-toggle{background-image:none!important}.bg-gradient-fuchsia.btn:hover{background:#f012be linear-gradient(180deg,#d631b1,#cf0da3) repeat-x!important;border-color:#c30c9a;color:#ececec}.bg-gradient-fuchsia.btn.active,.bg-gradient-fuchsia.btn:active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled).active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled):active{background:#f012be linear-gradient(180deg,#cc31a9,#c30c9a) repeat-x!important;border-color:#b70c90;color:#fff}.bg-gradient-maroon{background:#d81b60 linear-gradient(180deg,#de3d78,#d81b60) repeat-x!important;color:#fff}.bg-gradient-maroon.btn.disabled,.bg-gradient-maroon.btn:disabled,.bg-gradient-maroon.btn:not(:disabled):not(.disabled).active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-maroon.btn.dropdown-toggle{background-image:none!important}.bg-gradient-maroon.btn:hover{background:#d81b60 linear-gradient(180deg,#c13a6b,#b61751) repeat-x!important;border-color:#ab154c;color:#ececec}.bg-gradient-maroon.btn.active,.bg-gradient-maroon.btn:active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled).active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled):active{background:#d81b60 linear-gradient(180deg,#b73867,#ab154c) repeat-x!important;border-color:#9f1447;color:#fff}.bg-gradient-blue{background:#007bff linear-gradient(180deg,#268fff,#007bff) repeat-x!important;color:#fff}.bg-gradient-blue.btn.disabled,.bg-gradient-blue.btn:disabled,.bg-gradient-blue.btn:not(:disabled):not(.disabled).active,.bg-gradient-blue.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-blue.btn.dropdown-toggle{background-image:none!important}.bg-gradient-blue.btn:hover{background:#007bff linear-gradient(180deg,#267fde,#0069d9) repeat-x!important;border-color:#0062cc;color:#ececec}.bg-gradient-blue.btn.active,.bg-gradient-blue.btn:active,.bg-gradient-blue.btn:not(:disabled):not(.disabled).active,.bg-gradient-blue.btn:not(:disabled):not(.disabled):active{background:#007bff linear-gradient(180deg,#267ad4,#0062cc) repeat-x!important;border-color:#005cbf;color:#fff}.bg-gradient-indigo{background:#6610f2 linear-gradient(180deg,#7d34f4,#6610f2) repeat-x!important;color:#fff}.bg-gradient-indigo.btn.disabled,.bg-gradient-indigo.btn:disabled,.bg-gradient-indigo.btn:not(:disabled):not(.disabled).active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-indigo.btn.dropdown-toggle{background-image:none!important}.bg-gradient-indigo.btn:hover{background:#6610f2 linear-gradient(180deg,#7030d7,#560bd0) repeat-x!important;border-color:#510bc4;color:#ececec}.bg-gradient-indigo.btn.active,.bg-gradient-indigo.btn:active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled).active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled):active{background:#6610f2 linear-gradient(180deg,#6b2fcd,#510bc4) repeat-x!important;border-color:#4c0ab8;color:#fff}.bg-gradient-purple{background:#6f42c1 linear-gradient(180deg,#855eca,#6f42c1) repeat-x!important;color:#fff}.bg-gradient-purple.btn.disabled,.bg-gradient-purple.btn:disabled,.bg-gradient-purple.btn:not(:disabled):not(.disabled).active,.bg-gradient-purple.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-purple.btn.dropdown-toggle{background-image:none!important}.bg-gradient-purple.btn:hover{background:#6f42c1 linear-gradient(180deg,#7655b4,#5e37a6) repeat-x!important;border-color:#59339d;color:#ececec}.bg-gradient-purple.btn.active,.bg-gradient-purple.btn:active,.bg-gradient-purple.btn:not(:disabled):not(.disabled).active,.bg-gradient-purple.btn:not(:disabled):not(.disabled):active{background:#6f42c1 linear-gradient(180deg,#7252ab,#59339d) repeat-x!important;border-color:#533093;color:#fff}.bg-gradient-pink{background:#e83e8c linear-gradient(180deg,#eb5b9d,#e83e8c) repeat-x!important;color:#fff}.bg-gradient-pink.btn.disabled,.bg-gradient-pink.btn:disabled,.bg-gradient-pink.btn:not(:disabled):not(.disabled).active,.bg-gradient-pink.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-pink.btn.dropdown-toggle{background-image:none!important}.bg-gradient-pink.btn:hover{background:#e83e8c linear-gradient(180deg,#e83e8c,#e41c78) repeat-x!important;border-color:#d91a72;color:#ececec}.bg-gradient-pink.btn.active,.bg-gradient-pink.btn:active,.bg-gradient-pink.btn:not(:disabled):not(.disabled).active,.bg-gradient-pink.btn:not(:disabled):not(.disabled):active{background:#e83e8c linear-gradient(180deg,#df3c87,#d91a72) repeat-x!important;border-color:#ce196c;color:#fff}.bg-gradient-red{background:#dc3545 linear-gradient(180deg,#e15361,#dc3545) repeat-x!important;color:#fff}.bg-gradient-red.btn.disabled,.bg-gradient-red.btn:disabled,.bg-gradient-red.btn:not(:disabled):not(.disabled).active,.bg-gradient-red.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-red.btn.dropdown-toggle{background-image:none!important}.bg-gradient-red.btn:hover{background:#dc3545 linear-gradient(180deg,#d04451,#c82333) repeat-x!important;border-color:#bd2130;color:#ececec}.bg-gradient-red.btn.active,.bg-gradient-red.btn:active,.bg-gradient-red.btn:not(:disabled):not(.disabled).active,.bg-gradient-red.btn:not(:disabled):not(.disabled):active{background:#dc3545 linear-gradient(180deg,#c7424f,#bd2130) repeat-x!important;border-color:#b21f2d;color:#fff}.bg-gradient-orange{background:#fd7e14 linear-gradient(180deg,#fd9137,#fd7e14) repeat-x!important;color:#1f2d3d}.bg-gradient-orange.btn.disabled,.bg-gradient-orange.btn:disabled,.bg-gradient-orange.btn:not(:disabled):not(.disabled).active,.bg-gradient-orange.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-orange.btn.dropdown-toggle{background-image:none!important}.bg-gradient-orange.btn:hover{background:#fd7e14 linear-gradient(180deg,#ec8128,#e96b02) repeat-x!important;border-color:#dc6502;color:#121a24}.bg-gradient-orange.btn.active,.bg-gradient-orange.btn:active,.bg-gradient-orange.btn:not(:disabled):not(.disabled).active,.bg-gradient-orange.btn:not(:disabled):not(.disabled):active{background:#fd7e14 linear-gradient(180deg,#e17c28,#dc6502) repeat-x!important;border-color:#cf5f02;color:#fff}.bg-gradient-yellow{background:#ffc107 linear-gradient(180deg,#ffca2c,#ffc107) repeat-x!important;color:#1f2d3d}.bg-gradient-yellow.btn.disabled,.bg-gradient-yellow.btn:disabled,.bg-gradient-yellow.btn:not(:disabled):not(.disabled).active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-yellow.btn.dropdown-toggle{background-image:none!important}.bg-gradient-yellow.btn:hover{background:#ffc107 linear-gradient(180deg,#e4b526,#e0a800) repeat-x!important;border-color:#d39e00;color:#121a24}.bg-gradient-yellow.btn.active,.bg-gradient-yellow.btn:active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled).active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled):active{background:#ffc107 linear-gradient(180deg,#daad26,#d39e00) repeat-x!important;border-color:#c69500;color:#1f2d3d}.bg-gradient-green{background:#28a745 linear-gradient(180deg,#48b461,#28a745) repeat-x!important;color:#fff}.bg-gradient-green.btn.disabled,.bg-gradient-green.btn:disabled,.bg-gradient-green.btn:not(:disabled):not(.disabled).active,.bg-gradient-green.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-green.btn.dropdown-toggle{background-image:none!important}.bg-gradient-green.btn:hover{background:#28a745 linear-gradient(180deg,#429a56,#218838) repeat-x!important;border-color:#1e7e34;color:#ececec}.bg-gradient-green.btn.active,.bg-gradient-green.btn:active,.bg-gradient-green.btn:not(:disabled):not(.disabled).active,.bg-gradient-green.btn:not(:disabled):not(.disabled):active{background:#28a745 linear-gradient(180deg,#409152,#1e7e34) repeat-x!important;border-color:#1c7430;color:#fff}.bg-gradient-teal{background:#20c997 linear-gradient(180deg,#41d1a7,#20c997) repeat-x!important;color:#fff}.bg-gradient-teal.btn.disabled,.bg-gradient-teal.btn:disabled,.bg-gradient-teal.btn:not(:disabled):not(.disabled).active,.bg-gradient-teal.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-teal.btn.dropdown-toggle{background-image:none!important}.bg-gradient-teal.btn:hover{background:#20c997 linear-gradient(180deg,#3db592,#1ba87e) repeat-x!important;border-color:#199d76;color:#ececec}.bg-gradient-teal.btn.active,.bg-gradient-teal.btn:active,.bg-gradient-teal.btn:not(:disabled):not(.disabled).active,.bg-gradient-teal.btn:not(:disabled):not(.disabled):active{background:#20c997 linear-gradient(180deg,#3bac8b,#199d76) repeat-x!important;border-color:#17926e;color:#fff}.bg-gradient-cyan{background:#17a2b8 linear-gradient(180deg,#3ab0c3,#17a2b8) repeat-x!important;color:#fff}.bg-gradient-cyan.btn.disabled,.bg-gradient-cyan.btn:disabled,.bg-gradient-cyan.btn:not(:disabled):not(.disabled).active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-cyan.btn.dropdown-toggle{background-image:none!important}.bg-gradient-cyan.btn:hover{background:#17a2b8 linear-gradient(180deg,#3697a6,#138496) repeat-x!important;border-color:#117a8b;color:#ececec}.bg-gradient-cyan.btn.active,.bg-gradient-cyan.btn:active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled).active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled):active{background:#17a2b8 linear-gradient(180deg,#358e9c,#117a8b) repeat-x!important;border-color:#10707f;color:#fff}.bg-gradient-white{background:#fff linear-gradient(180deg,#fff,#fff) repeat-x!important;color:#1f2d3d}.bg-gradient-white.btn.disabled,.bg-gradient-white.btn:disabled,.bg-gradient-white.btn:not(:disabled):not(.disabled).active,.bg-gradient-white.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-white.btn.dropdown-toggle{background-image:none!important}.bg-gradient-white.btn:hover{background:#fff linear-gradient(180deg,#efefef,#ececec) repeat-x!important;border-color:#e6e6e6;color:#121a24}.bg-gradient-white.btn.active,.bg-gradient-white.btn:active,.bg-gradient-white.btn:not(:disabled):not(.disabled).active,.bg-gradient-white.btn:not(:disabled):not(.disabled):active{background:#fff linear-gradient(180deg,#e9e9e9,#e6e6e6) repeat-x!important;border-color:#dfdfdf;color:#1f2d3d}.bg-gradient-gray{background:#6c757d linear-gradient(180deg,#828a91,#6c757d) repeat-x!important;color:#fff}.bg-gradient-gray.btn.disabled,.bg-gradient-gray.btn:disabled,.bg-gradient-gray.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-gray.btn.dropdown-toggle{background-image:none!important}.bg-gradient-gray.btn:hover{background:#6c757d linear-gradient(180deg,#73797f,#5a6268) repeat-x!important;border-color:#545b62;color:#ececec}.bg-gradient-gray.btn.active,.bg-gradient-gray.btn:active,.bg-gradient-gray.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray.btn:not(:disabled):not(.disabled):active{background:#6c757d linear-gradient(180deg,#6e7479,#545b62) repeat-x!important;border-color:#4e555b;color:#fff}.bg-gradient-gray-dark{background:#343a40 linear-gradient(180deg,#52585d,#343a40) repeat-x!important;color:#fff}.bg-gradient-gray-dark.btn.disabled,.bg-gradient-gray-dark.btn:disabled,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-gray-dark.btn.dropdown-toggle{background-image:none!important}.bg-gradient-gray-dark.btn:hover{background:#343a40 linear-gradient(180deg,#44474b,#23272b) repeat-x!important;border-color:#1d2124;color:#ececec}.bg-gradient-gray-dark.btn.active,.bg-gradient-gray-dark.btn:active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled):active{background:#343a40 linear-gradient(180deg,#3f4245,#1d2124) repeat-x!important;border-color:#171a1d;color:#fff}[class^=bg-].disabled{opacity:.65}a.text-muted:hover{color:#007bff!important}.link-muted{color:#5d6974}.link-muted:focus,.link-muted:hover{color:#464f58}.link-black{color:#6c757d}.link-black:focus,.link-black:hover{color:#e6e8ea}.accent-primary .btn-link,.accent-primary .nav-tabs .nav-link,.accent-primary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#007bff}.accent-primary .btn-link:hover,.accent-primary .nav-tabs .nav-link:hover,.accent-primary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#0056b3}.accent-primary .dropdown-item.active,.accent-primary .dropdown-item:active{background-color:#007bff;color:#fff}.accent-primary .custom-control-input:checked~.custom-control-label::before{background-color:#007bff;border-color:#004a99}.accent-primary .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-primary .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-primary .custom-file-input:focus~.custom-file-label,.accent-primary .custom-select:focus,.accent-primary .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#80bdff}.accent-primary .page-item .page-link{color:#007bff}.accent-primary .page-item.active .page-link,.accent-primary .page-item.active a{background-color:#007bff;border-color:#007bff;color:#fff}.accent-primary .page-item.disabled .page-link,.accent-primary .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-primary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-primary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-primary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-primary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-primary .page-item .page-link:focus,.dark-mode.accent-primary .page-item .page-link:hover{color:#1a88ff}.accent-secondary .btn-link,.accent-secondary .nav-tabs .nav-link,.accent-secondary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6c757d}.accent-secondary .btn-link:hover,.accent-secondary .nav-tabs .nav-link:hover,.accent-secondary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#494f54}.accent-secondary .dropdown-item.active,.accent-secondary .dropdown-item:active{background-color:#6c757d;color:#fff}.accent-secondary .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.accent-secondary .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-secondary .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-secondary .custom-file-input:focus~.custom-file-label,.accent-secondary .custom-select:focus,.accent-secondary .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#afb5ba}.accent-secondary .page-item .page-link{color:#6c757d}.accent-secondary .page-item.active .page-link,.accent-secondary .page-item.active a{background-color:#6c757d;border-color:#6c757d;color:#fff}.accent-secondary .page-item.disabled .page-link,.accent-secondary .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-secondary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-secondary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-secondary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-secondary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-secondary .page-item .page-link:focus,.dark-mode.accent-secondary .page-item .page-link:hover{color:#78828a}.accent-success .btn-link,.accent-success .nav-tabs .nav-link,.accent-success a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#28a745}.accent-success .btn-link:hover,.accent-success .nav-tabs .nav-link:hover,.accent-success a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#19692c}.accent-success .dropdown-item.active,.accent-success .dropdown-item:active{background-color:#28a745;color:#fff}.accent-success .custom-control-input:checked~.custom-control-label::before{background-color:#28a745;border-color:#145523}.accent-success .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-success .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-success .custom-file-input:focus~.custom-file-label,.accent-success .custom-select:focus,.accent-success .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#71dd8a}.accent-success .page-item .page-link{color:#28a745}.accent-success .page-item.active .page-link,.accent-success .page-item.active a{background-color:#28a745;border-color:#28a745;color:#fff}.accent-success .page-item.disabled .page-link,.accent-success .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-success [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-success [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-success [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-success [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-success .page-item .page-link:focus,.dark-mode.accent-success .page-item .page-link:hover{color:#2dbc4e}.accent-info .btn-link,.accent-info .nav-tabs .nav-link,.accent-info a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#17a2b8}.accent-info .btn-link:hover,.accent-info .nav-tabs .nav-link:hover,.accent-info a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#0f6674}.accent-info .dropdown-item.active,.accent-info .dropdown-item:active{background-color:#17a2b8;color:#fff}.accent-info .custom-control-input:checked~.custom-control-label::before{background-color:#17a2b8;border-color:#0c525d}.accent-info .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-info .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-info .custom-file-input:focus~.custom-file-label,.accent-info .custom-select:focus,.accent-info .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#63d9ec}.accent-info .page-item .page-link{color:#17a2b8}.accent-info .page-item.active .page-link,.accent-info .page-item.active a{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.accent-info .page-item.disabled .page-link,.accent-info .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-info [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-info [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-info [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-info [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-info .page-item .page-link:focus,.dark-mode.accent-info .page-item .page-link:hover{color:#1ab6cf}.accent-warning .btn-link,.accent-warning .nav-tabs .nav-link,.accent-warning a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#ffc107}.accent-warning .btn-link:hover,.accent-warning .nav-tabs .nav-link:hover,.accent-warning a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#ba8b00}.accent-warning .dropdown-item.active,.accent-warning .dropdown-item:active{background-color:#ffc107;color:#1f2d3d}.accent-warning .custom-control-input:checked~.custom-control-label::before{background-color:#ffc107;border-color:#a07800}.accent-warning .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-warning .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-warning .custom-file-input:focus~.custom-file-label,.accent-warning .custom-select:focus,.accent-warning .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#ffe187}.accent-warning .page-item .page-link{color:#ffc107}.accent-warning .page-item.active .page-link,.accent-warning .page-item.active a{background-color:#ffc107;border-color:#ffc107;color:#fff}.accent-warning .page-item.disabled .page-link,.accent-warning .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-warning [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-warning [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-warning [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-warning [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-warning .page-item .page-link:focus,.dark-mode.accent-warning .page-item .page-link:hover{color:#ffc721}.accent-danger .btn-link,.accent-danger .nav-tabs .nav-link,.accent-danger a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#dc3545}.accent-danger .btn-link:hover,.accent-danger .nav-tabs .nav-link:hover,.accent-danger a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#a71d2a}.accent-danger .dropdown-item.active,.accent-danger .dropdown-item:active{background-color:#dc3545;color:#fff}.accent-danger .custom-control-input:checked~.custom-control-label::before{background-color:#dc3545;border-color:#921925}.accent-danger .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-danger .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-danger .custom-file-input:focus~.custom-file-label,.accent-danger .custom-select:focus,.accent-danger .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#efa2a9}.accent-danger .page-item .page-link{color:#dc3545}.accent-danger .page-item.active .page-link,.accent-danger .page-item.active a{background-color:#dc3545;border-color:#dc3545;color:#fff}.accent-danger .page-item.disabled .page-link,.accent-danger .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-danger [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-danger [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-danger [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-danger [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-danger .page-item .page-link:focus,.dark-mode.accent-danger .page-item .page-link:hover{color:#e04b59}.accent-light .btn-link,.accent-light .nav-tabs .nav-link,.accent-light a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#f8f9fa}.accent-light .btn-link:hover,.accent-light .nav-tabs .nav-link:hover,.accent-light a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#cbd3da}.accent-light .dropdown-item.active,.accent-light .dropdown-item:active{background-color:#f8f9fa;color:#1f2d3d}.accent-light .custom-control-input:checked~.custom-control-label::before{background-color:#f8f9fa;border-color:#bdc6d0}.accent-light .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-light .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-light .custom-file-input:focus~.custom-file-label,.accent-light .custom-select:focus,.accent-light .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fff}.accent-light .page-item .page-link{color:#f8f9fa}.accent-light .page-item.active .page-link,.accent-light .page-item.active a{background-color:#f8f9fa;border-color:#f8f9fa;color:#fff}.accent-light .page-item.disabled .page-link,.accent-light .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-light [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-light [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-light [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-light [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-light .page-item .page-link:focus,.dark-mode.accent-light .page-item .page-link:hover{color:#fff}.accent-dark .btn-link,.accent-dark .nav-tabs .nav-link,.accent-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#343a40}.accent-dark .btn-link:hover,.accent-dark .nav-tabs .nav-link:hover,.accent-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#121416}.accent-dark .dropdown-item.active,.accent-dark .dropdown-item:active{background-color:#343a40;color:#fff}.accent-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.accent-dark .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-dark .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-dark .custom-file-input:focus~.custom-file-label,.accent-dark .custom-select:focus,.accent-dark .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#6d7a86}.accent-dark .page-item .page-link{color:#343a40}.accent-dark .page-item.active .page-link,.accent-dark .page-item.active a{background-color:#343a40;border-color:#343a40;color:#fff}.accent-dark .page-item.disabled .page-link,.accent-dark .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-dark .page-item .page-link:focus,.dark-mode.accent-dark .page-item .page-link:hover{color:#3f474e}.accent-lightblue .btn-link,.accent-lightblue .nav-tabs .nav-link,.accent-lightblue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#3c8dbc}.accent-lightblue .btn-link:hover,.accent-lightblue .nav-tabs .nav-link:hover,.accent-lightblue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#296282}.accent-lightblue .dropdown-item.active,.accent-lightblue .dropdown-item:active{background-color:#3c8dbc;color:#fff}.accent-lightblue .custom-control-input:checked~.custom-control-label::before{background-color:#3c8dbc;border-color:#23536f}.accent-lightblue .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-lightblue .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-lightblue .custom-file-input:focus~.custom-file-label,.accent-lightblue .custom-select:focus,.accent-lightblue .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#99c5de}.accent-lightblue .page-item .page-link{color:#3c8dbc}.accent-lightblue .page-item.active .page-link,.accent-lightblue .page-item.active a{background-color:#3c8dbc;border-color:#3c8dbc;color:#fff}.accent-lightblue .page-item.disabled .page-link,.accent-lightblue .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-lightblue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-lightblue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-lightblue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-lightblue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-lightblue .page-item .page-link:focus,.dark-mode.accent-lightblue .page-item .page-link:hover{color:#4c99c6}.accent-navy .btn-link,.accent-navy .nav-tabs .nav-link,.accent-navy a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#001f3f}.accent-navy .btn-link:hover,.accent-navy .nav-tabs .nav-link:hover,.accent-navy a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#000}.accent-navy .dropdown-item.active,.accent-navy .dropdown-item:active{background-color:#001f3f;color:#fff}.accent-navy .custom-control-input:checked~.custom-control-label::before{background-color:#001f3f;border-color:#000}.accent-navy .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-navy .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-navy .custom-file-input:focus~.custom-file-label,.accent-navy .custom-select:focus,.accent-navy .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#005ebf}.accent-navy .page-item .page-link{color:#001f3f}.accent-navy .page-item.active .page-link,.accent-navy .page-item.active a{background-color:#001f3f;border-color:#001f3f;color:#fff}.accent-navy .page-item.disabled .page-link,.accent-navy .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-navy [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-navy [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-navy [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-navy [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-navy .page-item .page-link:focus,.dark-mode.accent-navy .page-item .page-link:hover{color:#002c59}.accent-olive .btn-link,.accent-olive .nav-tabs .nav-link,.accent-olive a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#3d9970}.accent-olive .btn-link:hover,.accent-olive .nav-tabs .nav-link:hover,.accent-olive a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#276248}.accent-olive .dropdown-item.active,.accent-olive .dropdown-item:active{background-color:#3d9970;color:#fff}.accent-olive .custom-control-input:checked~.custom-control-label::before{background-color:#3d9970;border-color:#20503b}.accent-olive .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-olive .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-olive .custom-file-input:focus~.custom-file-label,.accent-olive .custom-select:focus,.accent-olive .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#87cfaf}.accent-olive .page-item .page-link{color:#3d9970}.accent-olive .page-item.active .page-link,.accent-olive .page-item.active a{background-color:#3d9970;border-color:#3d9970;color:#fff}.accent-olive .page-item.disabled .page-link,.accent-olive .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-olive [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-olive [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-olive [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-olive [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-olive .page-item .page-link:focus,.dark-mode.accent-olive .page-item .page-link:hover{color:#44ab7d}.accent-lime .btn-link,.accent-lime .nav-tabs .nav-link,.accent-lime a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#01ff70}.accent-lime .btn-link:hover,.accent-lime .nav-tabs .nav-link:hover,.accent-lime a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#00b44e}.accent-lime .dropdown-item.active,.accent-lime .dropdown-item:active{background-color:#01ff70;color:#1f2d3d}.accent-lime .custom-control-input:checked~.custom-control-label::before{background-color:#01ff70;border-color:#009a43}.accent-lime .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-lime .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-lime .custom-file-input:focus~.custom-file-label,.accent-lime .custom-select:focus,.accent-lime .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#81ffb8}.accent-lime .page-item .page-link{color:#01ff70}.accent-lime .page-item.active .page-link,.accent-lime .page-item.active a{background-color:#01ff70;border-color:#01ff70;color:#fff}.accent-lime .page-item.disabled .page-link,.accent-lime .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-lime [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-lime [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-lime [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-lime [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-lime .page-item .page-link:focus,.dark-mode.accent-lime .page-item .page-link:hover{color:#1bff7e}.accent-fuchsia .btn-link,.accent-fuchsia .nav-tabs .nav-link,.accent-fuchsia a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#f012be}.accent-fuchsia .btn-link:hover,.accent-fuchsia .nav-tabs .nav-link:hover,.accent-fuchsia a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#ab0b87}.accent-fuchsia .dropdown-item.active,.accent-fuchsia .dropdown-item:active{background-color:#f012be;color:#fff}.accent-fuchsia .custom-control-input:checked~.custom-control-label::before{background-color:#f012be;border-color:#930974}.accent-fuchsia .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-fuchsia .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-fuchsia .custom-file-input:focus~.custom-file-label,.accent-fuchsia .custom-select:focus,.accent-fuchsia .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f88adf}.accent-fuchsia .page-item .page-link{color:#f012be}.accent-fuchsia .page-item.active .page-link,.accent-fuchsia .page-item.active a{background-color:#f012be;border-color:#f012be;color:#fff}.accent-fuchsia .page-item.disabled .page-link,.accent-fuchsia .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-fuchsia [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-fuchsia [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-fuchsia [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-fuchsia [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-fuchsia .page-item .page-link:focus,.dark-mode.accent-fuchsia .page-item .page-link:hover{color:#f22ac5}.accent-maroon .btn-link,.accent-maroon .nav-tabs .nav-link,.accent-maroon a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#d81b60}.accent-maroon .btn-link:hover,.accent-maroon .nav-tabs .nav-link:hover,.accent-maroon a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#941342}.accent-maroon .dropdown-item.active,.accent-maroon .dropdown-item:active{background-color:#d81b60;color:#fff}.accent-maroon .custom-control-input:checked~.custom-control-label::before{background-color:#d81b60;border-color:#7d1038}.accent-maroon .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-maroon .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-maroon .custom-file-input:focus~.custom-file-label,.accent-maroon .custom-select:focus,.accent-maroon .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f083ab}.accent-maroon .page-item .page-link{color:#d81b60}.accent-maroon .page-item.active .page-link,.accent-maroon .page-item.active a{background-color:#d81b60;border-color:#d81b60;color:#fff}.accent-maroon .page-item.disabled .page-link,.accent-maroon .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-maroon [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-maroon [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-maroon [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-maroon [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-maroon .page-item .page-link:focus,.dark-mode.accent-maroon .page-item .page-link:hover{color:#e4286d}.accent-blue .btn-link,.accent-blue .nav-tabs .nav-link,.accent-blue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#007bff}.accent-blue .btn-link:hover,.accent-blue .nav-tabs .nav-link:hover,.accent-blue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#0056b3}.accent-blue .dropdown-item.active,.accent-blue .dropdown-item:active{background-color:#007bff;color:#fff}.accent-blue .custom-control-input:checked~.custom-control-label::before{background-color:#007bff;border-color:#004a99}.accent-blue .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-blue .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-blue .custom-file-input:focus~.custom-file-label,.accent-blue .custom-select:focus,.accent-blue .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#80bdff}.accent-blue .page-item .page-link{color:#007bff}.accent-blue .page-item.active .page-link,.accent-blue .page-item.active a{background-color:#007bff;border-color:#007bff;color:#fff}.accent-blue .page-item.disabled .page-link,.accent-blue .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-blue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-blue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-blue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-blue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-blue .page-item .page-link:focus,.dark-mode.accent-blue .page-item .page-link:hover{color:#1a88ff}.accent-indigo .btn-link,.accent-indigo .nav-tabs .nav-link,.accent-indigo a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6610f2}.accent-indigo .btn-link:hover,.accent-indigo .nav-tabs .nav-link:hover,.accent-indigo a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#4709ac}.accent-indigo .dropdown-item.active,.accent-indigo .dropdown-item:active{background-color:#6610f2;color:#fff}.accent-indigo .custom-control-input:checked~.custom-control-label::before{background-color:#6610f2;border-color:#3d0894}.accent-indigo .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-indigo .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-indigo .custom-file-input:focus~.custom-file-label,.accent-indigo .custom-select:focus,.accent-indigo .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#b389f9}.accent-indigo .page-item .page-link{color:#6610f2}.accent-indigo .page-item.active .page-link,.accent-indigo .page-item.active a{background-color:#6610f2;border-color:#6610f2;color:#fff}.accent-indigo .page-item.disabled .page-link,.accent-indigo .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-indigo [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-indigo [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-indigo [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-indigo [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-indigo .page-item .page-link:focus,.dark-mode.accent-indigo .page-item .page-link:hover{color:#7528f3}.accent-purple .btn-link,.accent-purple .nav-tabs .nav-link,.accent-purple a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6f42c1}.accent-purple .btn-link:hover,.accent-purple .nav-tabs .nav-link:hover,.accent-purple a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#4e2d89}.accent-purple .dropdown-item.active,.accent-purple .dropdown-item:active{background-color:#6f42c1;color:#fff}.accent-purple .custom-control-input:checked~.custom-control-label::before{background-color:#6f42c1;border-color:#432776}.accent-purple .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-purple .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-purple .custom-file-input:focus~.custom-file-label,.accent-purple .custom-select:focus,.accent-purple .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#b8a2e0}.accent-purple .page-item .page-link{color:#6f42c1}.accent-purple .page-item.active .page-link,.accent-purple .page-item.active a{background-color:#6f42c1;border-color:#6f42c1;color:#fff}.accent-purple .page-item.disabled .page-link,.accent-purple .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-purple [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-purple [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-purple [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-purple [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-purple .page-item .page-link:focus,.dark-mode.accent-purple .page-item .page-link:hover{color:#7e55c7}.accent-pink .btn-link,.accent-pink .nav-tabs .nav-link,.accent-pink a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#e83e8c}.accent-pink .btn-link:hover,.accent-pink .nav-tabs .nav-link:hover,.accent-pink a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#c21766}.accent-pink .dropdown-item.active,.accent-pink .dropdown-item:active{background-color:#e83e8c;color:#fff}.accent-pink .custom-control-input:checked~.custom-control-label::before{background-color:#e83e8c;border-color:#ac145a}.accent-pink .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-pink .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-pink .custom-file-input:focus~.custom-file-label,.accent-pink .custom-select:focus,.accent-pink .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f6b0d0}.accent-pink .page-item .page-link{color:#e83e8c}.accent-pink .page-item.active .page-link,.accent-pink .page-item.active a{background-color:#e83e8c;border-color:#e83e8c;color:#fff}.accent-pink .page-item.disabled .page-link,.accent-pink .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-pink [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-pink [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-pink [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-pink [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-pink .page-item .page-link:focus,.dark-mode.accent-pink .page-item .page-link:hover{color:#eb559a}.accent-red .btn-link,.accent-red .nav-tabs .nav-link,.accent-red a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#dc3545}.accent-red .btn-link:hover,.accent-red .nav-tabs .nav-link:hover,.accent-red a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#a71d2a}.accent-red .dropdown-item.active,.accent-red .dropdown-item:active{background-color:#dc3545;color:#fff}.accent-red .custom-control-input:checked~.custom-control-label::before{background-color:#dc3545;border-color:#921925}.accent-red .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-red .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-red .custom-file-input:focus~.custom-file-label,.accent-red .custom-select:focus,.accent-red .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#efa2a9}.accent-red .page-item .page-link{color:#dc3545}.accent-red .page-item.active .page-link,.accent-red .page-item.active a{background-color:#dc3545;border-color:#dc3545;color:#fff}.accent-red .page-item.disabled .page-link,.accent-red .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-red [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-red [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-red [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-red [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-red .page-item .page-link:focus,.dark-mode.accent-red .page-item .page-link:hover{color:#e04b59}.accent-orange .btn-link,.accent-orange .nav-tabs .nav-link,.accent-orange a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#fd7e14}.accent-orange .btn-link:hover,.accent-orange .nav-tabs .nav-link:hover,.accent-orange a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#c35a02}.accent-orange .dropdown-item.active,.accent-orange .dropdown-item:active{background-color:#fd7e14;color:#1f2d3d}.accent-orange .custom-control-input:checked~.custom-control-label::before{background-color:#fd7e14;border-color:#aa4e01}.accent-orange .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-orange .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-orange .custom-file-input:focus~.custom-file-label,.accent-orange .custom-select:focus,.accent-orange .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fec392}.accent-orange .page-item .page-link{color:#fd7e14}.accent-orange .page-item.active .page-link,.accent-orange .page-item.active a{background-color:#fd7e14;border-color:#fd7e14;color:#fff}.accent-orange .page-item.disabled .page-link,.accent-orange .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-orange [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-orange [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-orange [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-orange [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-orange .page-item .page-link:focus,.dark-mode.accent-orange .page-item .page-link:hover{color:#fd8c2d}.accent-yellow .btn-link,.accent-yellow .nav-tabs .nav-link,.accent-yellow a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#ffc107}.accent-yellow .btn-link:hover,.accent-yellow .nav-tabs .nav-link:hover,.accent-yellow a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#ba8b00}.accent-yellow .dropdown-item.active,.accent-yellow .dropdown-item:active{background-color:#ffc107;color:#1f2d3d}.accent-yellow .custom-control-input:checked~.custom-control-label::before{background-color:#ffc107;border-color:#a07800}.accent-yellow .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-yellow .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-yellow .custom-file-input:focus~.custom-file-label,.accent-yellow .custom-select:focus,.accent-yellow .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#ffe187}.accent-yellow .page-item .page-link{color:#ffc107}.accent-yellow .page-item.active .page-link,.accent-yellow .page-item.active a{background-color:#ffc107;border-color:#ffc107;color:#fff}.accent-yellow .page-item.disabled .page-link,.accent-yellow .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-yellow [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-yellow [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-yellow [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-yellow [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-yellow .page-item .page-link:focus,.dark-mode.accent-yellow .page-item .page-link:hover{color:#ffc721}.accent-green .btn-link,.accent-green .nav-tabs .nav-link,.accent-green a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#28a745}.accent-green .btn-link:hover,.accent-green .nav-tabs .nav-link:hover,.accent-green a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#19692c}.accent-green .dropdown-item.active,.accent-green .dropdown-item:active{background-color:#28a745;color:#fff}.accent-green .custom-control-input:checked~.custom-control-label::before{background-color:#28a745;border-color:#145523}.accent-green .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-green .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-green .custom-file-input:focus~.custom-file-label,.accent-green .custom-select:focus,.accent-green .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#71dd8a}.accent-green .page-item .page-link{color:#28a745}.accent-green .page-item.active .page-link,.accent-green .page-item.active a{background-color:#28a745;border-color:#28a745;color:#fff}.accent-green .page-item.disabled .page-link,.accent-green .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-green [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-green [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-green [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-green [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-green .page-item .page-link:focus,.dark-mode.accent-green .page-item .page-link:hover{color:#2dbc4e}.accent-teal .btn-link,.accent-teal .nav-tabs .nav-link,.accent-teal a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#20c997}.accent-teal .btn-link:hover,.accent-teal .nav-tabs .nav-link:hover,.accent-teal a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#158765}.accent-teal .dropdown-item.active,.accent-teal .dropdown-item:active{background-color:#20c997;color:#fff}.accent-teal .custom-control-input:checked~.custom-control-label::before{background-color:#20c997;border-color:#127155}.accent-teal .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-teal .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-teal .custom-file-input:focus~.custom-file-label,.accent-teal .custom-select:focus,.accent-teal .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#7eeaca}.accent-teal .page-item .page-link{color:#20c997}.accent-teal .page-item.active .page-link,.accent-teal .page-item.active a{background-color:#20c997;border-color:#20c997;color:#fff}.accent-teal .page-item.disabled .page-link,.accent-teal .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-teal [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-teal [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-teal [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-teal [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-teal .page-item .page-link:focus,.dark-mode.accent-teal .page-item .page-link:hover{color:#26dca6}.accent-cyan .btn-link,.accent-cyan .nav-tabs .nav-link,.accent-cyan a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#17a2b8}.accent-cyan .btn-link:hover,.accent-cyan .nav-tabs .nav-link:hover,.accent-cyan a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#0f6674}.accent-cyan .dropdown-item.active,.accent-cyan .dropdown-item:active{background-color:#17a2b8;color:#fff}.accent-cyan .custom-control-input:checked~.custom-control-label::before{background-color:#17a2b8;border-color:#0c525d}.accent-cyan .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-cyan .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-cyan .custom-file-input:focus~.custom-file-label,.accent-cyan .custom-select:focus,.accent-cyan .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#63d9ec}.accent-cyan .page-item .page-link{color:#17a2b8}.accent-cyan .page-item.active .page-link,.accent-cyan .page-item.active a{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.accent-cyan .page-item.disabled .page-link,.accent-cyan .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-cyan [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-cyan [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-cyan [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-cyan [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-cyan .page-item .page-link:focus,.dark-mode.accent-cyan .page-item .page-link:hover{color:#1ab6cf}.accent-white .btn-link,.accent-white .nav-tabs .nav-link,.accent-white a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#fff}.accent-white .btn-link:hover,.accent-white .nav-tabs .nav-link:hover,.accent-white a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#d9d9d9}.accent-white .dropdown-item.active,.accent-white .dropdown-item:active{background-color:#fff;color:#1f2d3d}.accent-white .custom-control-input:checked~.custom-control-label::before{background-color:#fff;border-color:#ccc}.accent-white .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-white .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-white .custom-file-input:focus~.custom-file-label,.accent-white .custom-select:focus,.accent-white .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fff}.accent-white .page-item .page-link{color:#fff}.accent-white .page-item.active .page-link,.accent-white .page-item.active a{background-color:#fff;border-color:#fff;color:#fff}.accent-white .page-item.disabled .page-link,.accent-white .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-white [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-white [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-white [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-white [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-white .page-item .page-link:focus,.dark-mode.accent-white .page-item .page-link:hover{color:#fff}.accent-gray .btn-link,.accent-gray .nav-tabs .nav-link,.accent-gray a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6c757d}.accent-gray .btn-link:hover,.accent-gray .nav-tabs .nav-link:hover,.accent-gray a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#494f54}.accent-gray .dropdown-item.active,.accent-gray .dropdown-item:active{background-color:#6c757d;color:#fff}.accent-gray .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.accent-gray .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-gray .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-gray .custom-file-input:focus~.custom-file-label,.accent-gray .custom-select:focus,.accent-gray .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#afb5ba}.accent-gray .page-item .page-link{color:#6c757d}.accent-gray .page-item.active .page-link,.accent-gray .page-item.active a{background-color:#6c757d;border-color:#6c757d;color:#fff}.accent-gray .page-item.disabled .page-link,.accent-gray .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-gray [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-gray [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-gray [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-gray [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-gray .page-item .page-link:focus,.dark-mode.accent-gray .page-item .page-link:hover{color:#78828a}.accent-gray-dark .btn-link,.accent-gray-dark .nav-tabs .nav-link,.accent-gray-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#343a40}.accent-gray-dark .btn-link:hover,.accent-gray-dark .nav-tabs .nav-link:hover,.accent-gray-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#121416}.accent-gray-dark .dropdown-item.active,.accent-gray-dark .dropdown-item:active{background-color:#343a40;color:#fff}.accent-gray-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.accent-gray-dark .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-gray-dark .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-gray-dark .custom-file-input:focus~.custom-file-label,.accent-gray-dark .custom-select:focus,.accent-gray-dark .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#6d7a86}.accent-gray-dark .page-item .page-link{color:#343a40}.accent-gray-dark .page-item.active .page-link,.accent-gray-dark .page-item.active a{background-color:#343a40;border-color:#343a40;color:#fff}.accent-gray-dark .page-item.disabled .page-link,.accent-gray-dark .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-gray-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-gray-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-gray-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-gray-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode.accent-gray-dark .page-item .page-link:focus,.dark-mode.accent-gray-dark .page-item .page-link:hover{color:#3f474e}[class*=accent-] a.btn-primary{color:#fff}[class*=accent-] a.btn-secondary{color:#fff}[class*=accent-] a.btn-success{color:#fff}[class*=accent-] a.btn-info{color:#fff}[class*=accent-] a.btn-warning{color:#1f2d3d}[class*=accent-] a.btn-danger{color:#fff}[class*=accent-] a.btn-light{color:#1f2d3d}[class*=accent-] a.btn-dark{color:#fff}.dark-mode .bg-light{background-color:#454d55!important;color:#fff!important}.dark-mode .link-black,.dark-mode .link-dark,.dark-mode .text-black,.dark-mode .text-dark{color:#ced4da}.dark-mode .bg-primary{background-color:#3f6791!important}.dark-mode .bg-primary,.dark-mode .bg-primary>a{color:#fff!important}.dark-mode .bg-primary.btn:hover{border-color:#304e6d;color:#ececec}.dark-mode .bg-primary.btn.active,.dark-mode .bg-primary.btn:active,.dark-mode .bg-primary.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-primary.btn:not(:disabled):not(.disabled):active{background-color:#304e6d!important;border-color:#2c4765;color:#fff}.dark-mode .bg-secondary{background-color:#6c757d!important}.dark-mode .bg-secondary,.dark-mode .bg-secondary>a{color:#fff!important}.dark-mode .bg-secondary.btn:hover{border-color:#545b62;color:#ececec}.dark-mode .bg-secondary.btn.active,.dark-mode .bg-secondary.btn:active,.dark-mode .bg-secondary.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-secondary.btn:not(:disabled):not(.disabled):active{background-color:#545b62!important;border-color:#4e555b;color:#fff}.dark-mode .bg-success{background-color:#00bc8c!important}.dark-mode .bg-success,.dark-mode .bg-success>a{color:#fff!important}.dark-mode .bg-success.btn:hover{border-color:#008966;color:#ececec}.dark-mode .bg-success.btn.active,.dark-mode .bg-success.btn:active,.dark-mode .bg-success.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-success.btn:not(:disabled):not(.disabled):active{background-color:#008966!important;border-color:#007c5d;color:#fff}.dark-mode .bg-info{background-color:#3498db!important}.dark-mode .bg-info,.dark-mode .bg-info>a{color:#fff!important}.dark-mode .bg-info.btn:hover{border-color:#217dbb;color:#ececec}.dark-mode .bg-info.btn.active,.dark-mode .bg-info.btn:active,.dark-mode .bg-info.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-info.btn:not(:disabled):not(.disabled):active{background-color:#217dbb!important;border-color:#1f76b0;color:#fff}.dark-mode .bg-warning{background-color:#f39c12!important}.dark-mode .bg-warning,.dark-mode .bg-warning>a{color:#1f2d3d!important}.dark-mode .bg-warning.btn:hover{border-color:#c87f0a;color:#121a24}.dark-mode .bg-warning.btn.active,.dark-mode .bg-warning.btn:active,.dark-mode .bg-warning.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-warning.btn:not(:disabled):not(.disabled):active{background-color:#c87f0a!important;border-color:#bc770a;color:#fff}.dark-mode .bg-danger{background-color:#e74c3c!important}.dark-mode .bg-danger,.dark-mode .bg-danger>a{color:#fff!important}.dark-mode .bg-danger.btn:hover{border-color:#d62c1a;color:#ececec}.dark-mode .bg-danger.btn.active,.dark-mode .bg-danger.btn:active,.dark-mode .bg-danger.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-danger.btn:not(:disabled):not(.disabled):active{background-color:#d62c1a!important;border-color:#ca2a19;color:#fff}.dark-mode .bg-light{background-color:#f8f9fa!important}.dark-mode .bg-light,.dark-mode .bg-light>a{color:#1f2d3d!important}.dark-mode .bg-light.btn:hover{border-color:#dae0e5;color:#121a24}.dark-mode .bg-light.btn.active,.dark-mode .bg-light.btn:active,.dark-mode .bg-light.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-light.btn:not(:disabled):not(.disabled):active{background-color:#dae0e5!important;border-color:#d3d9df;color:#1f2d3d}.dark-mode .bg-dark{background-color:#343a40!important}.dark-mode .bg-dark,.dark-mode .bg-dark>a{color:#fff!important}.dark-mode .bg-dark.btn:hover{border-color:#1d2124;color:#ececec}.dark-mode .bg-dark.btn.active,.dark-mode .bg-dark.btn:active,.dark-mode .bg-dark.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-dark.btn:not(:disabled):not(.disabled):active{background-color:#1d2124!important;border-color:#171a1d;color:#fff}.dark-mode .bg-lightblue{background-color:#86bad8!important}.dark-mode .bg-lightblue,.dark-mode .bg-lightblue>a{color:#1f2d3d!important}.dark-mode .bg-lightblue.btn:hover{border-color:#5fa4cc;color:#121a24}.dark-mode .bg-lightblue.btn.active,.dark-mode .bg-lightblue.btn:active,.dark-mode .bg-lightblue.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-lightblue.btn:not(:disabled):not(.disabled):active{background-color:#5fa4cc!important;border-color:#559ec9;color:#fff}.dark-mode .bg-navy{background-color:#002c59!important}.dark-mode .bg-navy,.dark-mode .bg-navy>a{color:#fff!important}.dark-mode .bg-navy.btn:hover{border-color:#001226;color:#ececec}.dark-mode .bg-navy.btn.active,.dark-mode .bg-navy.btn:active,.dark-mode .bg-navy.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-navy.btn:not(:disabled):not(.disabled):active{background-color:#001226!important;border-color:#000c19;color:#fff}.dark-mode .bg-olive{background-color:#74c8a3!important}.dark-mode .bg-olive,.dark-mode .bg-olive>a{color:#1f2d3d!important}.dark-mode .bg-olive.btn:hover{border-color:#50b98a;color:#121a24}.dark-mode .bg-olive.btn.active,.dark-mode .bg-olive.btn:active,.dark-mode .bg-olive.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-olive.btn:not(:disabled):not(.disabled):active{background-color:#50b98a!important;border-color:#48b484;color:#fff}.dark-mode .bg-lime{background-color:#67ffa9!important}.dark-mode .bg-lime,.dark-mode .bg-lime>a{color:#1f2d3d!important}.dark-mode .bg-lime.btn:hover{border-color:#34ff8d;color:#121a24}.dark-mode .bg-lime.btn.active,.dark-mode .bg-lime.btn:active,.dark-mode .bg-lime.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-lime.btn:not(:disabled):not(.disabled):active{background-color:#34ff8d!important;border-color:#27ff86;color:#1f2d3d}.dark-mode .bg-fuchsia{background-color:#f672d8!important}.dark-mode .bg-fuchsia,.dark-mode .bg-fuchsia>a{color:#1f2d3d!important}.dark-mode .bg-fuchsia.btn:hover{border-color:#f342cb;color:#121a24}.dark-mode .bg-fuchsia.btn.active,.dark-mode .bg-fuchsia.btn:active,.dark-mode .bg-fuchsia.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-fuchsia.btn:not(:disabled):not(.disabled):active{background-color:#f342cb!important;border-color:#f236c8;color:#fff}.dark-mode .bg-maroon{background-color:#ed6c9b!important}.dark-mode .bg-maroon,.dark-mode .bg-maroon>a{color:#1f2d3d!important}.dark-mode .bg-maroon.btn:hover{border-color:#e73f7c;color:#121a24}.dark-mode .bg-maroon.btn.active,.dark-mode .bg-maroon.btn:active,.dark-mode .bg-maroon.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-maroon.btn:not(:disabled):not(.disabled):active{background-color:#e73f7c!important;border-color:#e63475;color:#fff}.dark-mode .bg-blue{background-color:#3f6791!important}.dark-mode .bg-blue,.dark-mode .bg-blue>a{color:#fff!important}.dark-mode .bg-blue.btn:hover{border-color:#304e6d;color:#ececec}.dark-mode .bg-blue.btn.active,.dark-mode .bg-blue.btn:active,.dark-mode .bg-blue.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-blue.btn:not(:disabled):not(.disabled):active{background-color:#304e6d!important;border-color:#2c4765;color:#fff}.dark-mode .bg-indigo{background-color:#6610f2!important}.dark-mode .bg-indigo,.dark-mode .bg-indigo>a{color:#fff!important}.dark-mode .bg-indigo.btn:hover{border-color:#510bc4;color:#ececec}.dark-mode .bg-indigo.btn.active,.dark-mode .bg-indigo.btn:active,.dark-mode .bg-indigo.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-indigo.btn:not(:disabled):not(.disabled):active{background-color:#510bc4!important;border-color:#4c0ab8;color:#fff}.dark-mode .bg-purple{background-color:#6f42c1!important}.dark-mode .bg-purple,.dark-mode .bg-purple>a{color:#fff!important}.dark-mode .bg-purple.btn:hover{border-color:#59339d;color:#ececec}.dark-mode .bg-purple.btn.active,.dark-mode .bg-purple.btn:active,.dark-mode .bg-purple.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-purple.btn:not(:disabled):not(.disabled):active{background-color:#59339d!important;border-color:#533093;color:#fff}.dark-mode .bg-pink{background-color:#e83e8c!important}.dark-mode .bg-pink,.dark-mode .bg-pink>a{color:#fff!important}.dark-mode .bg-pink.btn:hover{border-color:#d91a72;color:#ececec}.dark-mode .bg-pink.btn.active,.dark-mode .bg-pink.btn:active,.dark-mode .bg-pink.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-pink.btn:not(:disabled):not(.disabled):active{background-color:#d91a72!important;border-color:#ce196c;color:#fff}.dark-mode .bg-red{background-color:#e74c3c!important}.dark-mode .bg-red,.dark-mode .bg-red>a{color:#fff!important}.dark-mode .bg-red.btn:hover{border-color:#d62c1a;color:#ececec}.dark-mode .bg-red.btn.active,.dark-mode .bg-red.btn:active,.dark-mode .bg-red.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-red.btn:not(:disabled):not(.disabled):active{background-color:#d62c1a!important;border-color:#ca2a19;color:#fff}.dark-mode .bg-orange{background-color:#fd7e14!important}.dark-mode .bg-orange,.dark-mode .bg-orange>a{color:#1f2d3d!important}.dark-mode .bg-orange.btn:hover{border-color:#dc6502;color:#121a24}.dark-mode .bg-orange.btn.active,.dark-mode .bg-orange.btn:active,.dark-mode .bg-orange.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-orange.btn:not(:disabled):not(.disabled):active{background-color:#dc6502!important;border-color:#cf5f02;color:#fff}.dark-mode .bg-yellow{background-color:#f39c12!important}.dark-mode .bg-yellow,.dark-mode .bg-yellow>a{color:#1f2d3d!important}.dark-mode .bg-yellow.btn:hover{border-color:#c87f0a;color:#121a24}.dark-mode .bg-yellow.btn.active,.dark-mode .bg-yellow.btn:active,.dark-mode .bg-yellow.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-yellow.btn:not(:disabled):not(.disabled):active{background-color:#c87f0a!important;border-color:#bc770a;color:#fff}.dark-mode .bg-green{background-color:#00bc8c!important}.dark-mode .bg-green,.dark-mode .bg-green>a{color:#fff!important}.dark-mode .bg-green.btn:hover{border-color:#008966;color:#ececec}.dark-mode .bg-green.btn.active,.dark-mode .bg-green.btn:active,.dark-mode .bg-green.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-green.btn:not(:disabled):not(.disabled):active{background-color:#008966!important;border-color:#007c5d;color:#fff}.dark-mode .bg-teal{background-color:#20c997!important}.dark-mode .bg-teal,.dark-mode .bg-teal>a{color:#fff!important}.dark-mode .bg-teal.btn:hover{border-color:#199d76;color:#ececec}.dark-mode .bg-teal.btn.active,.dark-mode .bg-teal.btn:active,.dark-mode .bg-teal.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-teal.btn:not(:disabled):not(.disabled):active{background-color:#199d76!important;border-color:#17926e;color:#fff}.dark-mode .bg-cyan{background-color:#3498db!important}.dark-mode .bg-cyan,.dark-mode .bg-cyan>a{color:#fff!important}.dark-mode .bg-cyan.btn:hover{border-color:#217dbb;color:#ececec}.dark-mode .bg-cyan.btn.active,.dark-mode .bg-cyan.btn:active,.dark-mode .bg-cyan.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-cyan.btn:not(:disabled):not(.disabled):active{background-color:#217dbb!important;border-color:#1f76b0;color:#fff}.dark-mode .bg-white{background-color:#fff!important}.dark-mode .bg-white,.dark-mode .bg-white>a{color:#1f2d3d!important}.dark-mode .bg-white.btn:hover{border-color:#e6e6e6;color:#121a24}.dark-mode .bg-white.btn.active,.dark-mode .bg-white.btn:active,.dark-mode .bg-white.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-white.btn:not(:disabled):not(.disabled):active{background-color:#e6e6e6!important;border-color:#dfdfdf;color:#1f2d3d}.dark-mode .bg-gray{background-color:#6c757d!important}.dark-mode .bg-gray,.dark-mode .bg-gray>a{color:#fff!important}.dark-mode .bg-gray.btn:hover{border-color:#545b62;color:#ececec}.dark-mode .bg-gray.btn.active,.dark-mode .bg-gray.btn:active,.dark-mode .bg-gray.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gray.btn:not(:disabled):not(.disabled):active{background-color:#545b62!important;border-color:#4e555b;color:#fff}.dark-mode .bg-gray-dark{background-color:#343a40!important}.dark-mode .bg-gray-dark,.dark-mode .bg-gray-dark>a{color:#fff!important}.dark-mode .bg-gray-dark.btn:hover{border-color:#1d2124;color:#ececec}.dark-mode .bg-gray-dark.btn.active,.dark-mode .bg-gray-dark.btn:active,.dark-mode .bg-gray-dark.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gray-dark.btn:not(:disabled):not(.disabled):active{background-color:#1d2124!important;border-color:#171a1d;color:#fff}.dark-mode .bg-gradient-primary{background:#3f6791 linear-gradient(180deg,#5c7ea2,#3f6791) repeat-x!important;color:#fff}.dark-mode .bg-gradient-primary.btn.disabled,.dark-mode .bg-gradient-primary.btn:disabled,.dark-mode .bg-gradient-primary.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-primary.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-primary.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-primary.btn:hover{background:#3f6791 linear-gradient(180deg,#526e8b,#335476) repeat-x!important;border-color:#304e6d;color:#ececec}.dark-mode .bg-gradient-primary.btn.active,.dark-mode .bg-gradient-primary.btn:active,.dark-mode .bg-gradient-primary.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-primary.btn:not(:disabled):not(.disabled):active{background:#3f6791 linear-gradient(180deg,#4f6883,#304e6d) repeat-x!important;border-color:#2c4765;color:#fff}.dark-mode .bg-gradient-secondary{background:#6c757d linear-gradient(180deg,#828a91,#6c757d) repeat-x!important;color:#fff}.dark-mode .bg-gradient-secondary.btn.disabled,.dark-mode .bg-gradient-secondary.btn:disabled,.dark-mode .bg-gradient-secondary.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-secondary.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-secondary.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-secondary.btn:hover{background:#6c757d linear-gradient(180deg,#73797f,#5a6268) repeat-x!important;border-color:#545b62;color:#ececec}.dark-mode .bg-gradient-secondary.btn.active,.dark-mode .bg-gradient-secondary.btn:active,.dark-mode .bg-gradient-secondary.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-secondary.btn:not(:disabled):not(.disabled):active{background:#6c757d linear-gradient(180deg,#6e7479,#545b62) repeat-x!important;border-color:#4e555b;color:#fff}.dark-mode .bg-gradient-success{background:#00bc8c linear-gradient(180deg,#26c69d,#00bc8c) repeat-x!important;color:#fff}.dark-mode .bg-gradient-success.btn.disabled,.dark-mode .bg-gradient-success.btn:disabled,.dark-mode .bg-gradient-success.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-success.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-success.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-success.btn:hover{background:#00bc8c linear-gradient(180deg,#26a685,#009670) repeat-x!important;border-color:#008966;color:#ececec}.dark-mode .bg-gradient-success.btn.active,.dark-mode .bg-gradient-success.btn:active,.dark-mode .bg-gradient-success.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-success.btn:not(:disabled):not(.disabled):active{background:#00bc8c linear-gradient(180deg,#269b7d,#008966) repeat-x!important;border-color:#007c5d;color:#fff}.dark-mode .bg-gradient-info{background:#3498db linear-gradient(180deg,#52a7e0,#3498db) repeat-x!important;color:#fff}.dark-mode .bg-gradient-info.btn.disabled,.dark-mode .bg-gradient-info.btn:disabled,.dark-mode .bg-gradient-info.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-info.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-info.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-info.btn:hover{background:#3498db linear-gradient(180deg,#4497ce,#2384c6) repeat-x!important;border-color:#217dbb;color:#ececec}.dark-mode .bg-gradient-info.btn.active,.dark-mode .bg-gradient-info.btn:active,.dark-mode .bg-gradient-info.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-info.btn:not(:disabled):not(.disabled):active{background:#3498db linear-gradient(180deg,#4291c5,#217dbb) repeat-x!important;border-color:#1f76b0;color:#fff}.dark-mode .bg-gradient-warning{background:#f39c12 linear-gradient(180deg,#f5ab36,#f39c12) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-warning.btn.disabled,.dark-mode .bg-gradient-warning.btn:disabled,.dark-mode .bg-gradient-warning.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-warning.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-warning.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-warning.btn:hover{background:#f39c12 linear-gradient(180deg,#da982f,#d4860b) repeat-x!important;border-color:#c87f0a;color:#121a24}.dark-mode .bg-gradient-warning.btn.active,.dark-mode .bg-gradient-warning.btn:active,.dark-mode .bg-gradient-warning.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-warning.btn:not(:disabled):not(.disabled):active{background:#f39c12 linear-gradient(180deg,#d0922f,#c87f0a) repeat-x!important;border-color:#bc770a;color:#fff}.dark-mode .bg-gradient-danger{background:#e74c3c linear-gradient(180deg,#eb6759,#e74c3c) repeat-x!important;color:#fff}.dark-mode .bg-gradient-danger.btn.disabled,.dark-mode .bg-gradient-danger.btn:disabled,.dark-mode .bg-gradient-danger.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-danger.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-danger.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-danger.btn:hover{background:#e74c3c linear-gradient(180deg,#e64d3e,#e12e1c) repeat-x!important;border-color:#d62c1a;color:#ececec}.dark-mode .bg-gradient-danger.btn.active,.dark-mode .bg-gradient-danger.btn:active,.dark-mode .bg-gradient-danger.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-danger.btn:not(:disabled):not(.disabled):active{background:#e74c3c linear-gradient(180deg,#dc4c3d,#d62c1a) repeat-x!important;border-color:#ca2a19;color:#fff}.dark-mode .bg-gradient-light{background:#f8f9fa linear-gradient(180deg,#f9fafb,#f8f9fa) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-light.btn.disabled,.dark-mode .bg-gradient-light.btn:disabled,.dark-mode .bg-gradient-light.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-light.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-light.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-light.btn:hover{background:#f8f9fa linear-gradient(180deg,#e6eaed,#e2e6ea) repeat-x!important;border-color:#dae0e5;color:#121a24}.dark-mode .bg-gradient-light.btn.active,.dark-mode .bg-gradient-light.btn:active,.dark-mode .bg-gradient-light.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-light.btn:not(:disabled):not(.disabled):active{background:#f8f9fa linear-gradient(180deg,#e0e4e9,#dae0e5) repeat-x!important;border-color:#d3d9df;color:#1f2d3d}.dark-mode .bg-gradient-dark{background:#343a40 linear-gradient(180deg,#52585d,#343a40) repeat-x!important;color:#fff}.dark-mode .bg-gradient-dark.btn.disabled,.dark-mode .bg-gradient-dark.btn:disabled,.dark-mode .bg-gradient-dark.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-dark.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-dark.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-dark.btn:hover{background:#343a40 linear-gradient(180deg,#44474b,#23272b) repeat-x!important;border-color:#1d2124;color:#ececec}.dark-mode .bg-gradient-dark.btn.active,.dark-mode .bg-gradient-dark.btn:active,.dark-mode .bg-gradient-dark.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-dark.btn:not(:disabled):not(.disabled):active{background:#343a40 linear-gradient(180deg,#3f4245,#1d2124) repeat-x!important;border-color:#171a1d;color:#fff}.dark-mode .bg-gradient-lightblue{background:#86bad8 linear-gradient(180deg,#98c4de,#86bad8) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-lightblue.btn.disabled,.dark-mode .bg-gradient-lightblue.btn:disabled,.dark-mode .bg-gradient-lightblue.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-lightblue.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-lightblue.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-lightblue.btn:hover{background:#86bad8 linear-gradient(180deg,#7fb6d6,#69a9cf) repeat-x!important;border-color:#5fa4cc;color:#121a24}.dark-mode .bg-gradient-lightblue.btn.active,.dark-mode .bg-gradient-lightblue.btn:active,.dark-mode .bg-gradient-lightblue.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-lightblue.btn:not(:disabled):not(.disabled):active{background:#86bad8 linear-gradient(180deg,#77b2d4,#5fa4cc) repeat-x!important;border-color:#559ec9;color:#fff}.dark-mode .bg-gradient-navy{background:#002c59 linear-gradient(180deg,#264b71,#002c59) repeat-x!important;color:#fff}.dark-mode .bg-gradient-navy.btn.disabled,.dark-mode .bg-gradient-navy.btn:disabled,.dark-mode .bg-gradient-navy.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-navy.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-navy.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-navy.btn:hover{background:#002c59 linear-gradient(180deg,#263b51,#001932) repeat-x!important;border-color:#001226;color:#ececec}.dark-mode .bg-gradient-navy.btn.active,.dark-mode .bg-gradient-navy.btn:active,.dark-mode .bg-gradient-navy.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-navy.btn:not(:disabled):not(.disabled):active{background:#002c59 linear-gradient(180deg,#263646,#001226) repeat-x!important;border-color:#000c19;color:#fff}.dark-mode .bg-gradient-olive{background:#74c8a3 linear-gradient(180deg,#89d0b0,#74c8a3) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-olive.btn.disabled,.dark-mode .bg-gradient-olive.btn:disabled,.dark-mode .bg-gradient-olive.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-olive.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-olive.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-olive.btn:hover{background:#74c8a3 linear-gradient(180deg,#72c7a1,#59bd90) repeat-x!important;border-color:#50b98a;color:#121a24}.dark-mode .bg-gradient-olive.btn.active,.dark-mode .bg-gradient-olive.btn:active,.dark-mode .bg-gradient-olive.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-olive.btn:not(:disabled):not(.disabled):active{background:#74c8a3 linear-gradient(180deg,#6ac49c,#50b98a) repeat-x!important;border-color:#48b484;color:#fff}.dark-mode .bg-gradient-lime{background:#67ffa9 linear-gradient(180deg,#7effb6,#67ffa9) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-lime.btn.disabled,.dark-mode .bg-gradient-lime.btn:disabled,.dark-mode .bg-gradient-lime.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-lime.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-lime.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-lime.btn:hover{background:#67ffa9 linear-gradient(180deg,#5dffa4,#41ff94) repeat-x!important;border-color:#34ff8d;color:#121a24}.dark-mode .bg-gradient-lime.btn.active,.dark-mode .bg-gradient-lime.btn:active,.dark-mode .bg-gradient-lime.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-lime.btn:not(:disabled):not(.disabled):active{background:#67ffa9 linear-gradient(180deg,#52ff9e,#34ff8d) repeat-x!important;border-color:#27ff86;color:#1f2d3d}.dark-mode .bg-gradient-fuchsia{background:#f672d8 linear-gradient(180deg,#f787de,#f672d8) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-fuchsia.btn.disabled,.dark-mode .bg-gradient-fuchsia.btn:disabled,.dark-mode .bg-gradient-fuchsia.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-fuchsia.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-fuchsia.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-fuchsia.btn:hover{background:#f672d8 linear-gradient(180deg,#f569d6,#f44ece) repeat-x!important;border-color:#f342cb;color:#121a24}.dark-mode .bg-gradient-fuchsia.btn.active,.dark-mode .bg-gradient-fuchsia.btn:active,.dark-mode .bg-gradient-fuchsia.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-fuchsia.btn:not(:disabled):not(.disabled):active{background:#f672d8 linear-gradient(180deg,#f55ed3,#f342cb) repeat-x!important;border-color:#f236c8;color:#fff}.dark-mode .bg-gradient-maroon{background:#ed6c9b linear-gradient(180deg,#ef82aa,#ed6c9b) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-maroon.btn.disabled,.dark-mode .bg-gradient-maroon.btn:disabled,.dark-mode .bg-gradient-maroon.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-maroon.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-maroon.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-maroon.btn:hover{background:#ed6c9b linear-gradient(180deg,#ec6596,#e84a84) repeat-x!important;border-color:#e73f7c;color:#121a24}.dark-mode .bg-gradient-maroon.btn.active,.dark-mode .bg-gradient-maroon.btn:active,.dark-mode .bg-gradient-maroon.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-maroon.btn:not(:disabled):not(.disabled):active{background:#ed6c9b linear-gradient(180deg,#eb5c90,#e73f7c) repeat-x!important;border-color:#e63475;color:#fff}.dark-mode .bg-gradient-blue{background:#3f6791 linear-gradient(180deg,#5c7ea2,#3f6791) repeat-x!important;color:#fff}.dark-mode .bg-gradient-blue.btn.disabled,.dark-mode .bg-gradient-blue.btn:disabled,.dark-mode .bg-gradient-blue.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-blue.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-blue.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-blue.btn:hover{background:#3f6791 linear-gradient(180deg,#526e8b,#335476) repeat-x!important;border-color:#304e6d;color:#ececec}.dark-mode .bg-gradient-blue.btn.active,.dark-mode .bg-gradient-blue.btn:active,.dark-mode .bg-gradient-blue.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-blue.btn:not(:disabled):not(.disabled):active{background:#3f6791 linear-gradient(180deg,#4f6883,#304e6d) repeat-x!important;border-color:#2c4765;color:#fff}.dark-mode .bg-gradient-indigo{background:#6610f2 linear-gradient(180deg,#7d34f4,#6610f2) repeat-x!important;color:#fff}.dark-mode .bg-gradient-indigo.btn.disabled,.dark-mode .bg-gradient-indigo.btn:disabled,.dark-mode .bg-gradient-indigo.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-indigo.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-indigo.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-indigo.btn:hover{background:#6610f2 linear-gradient(180deg,#7030d7,#560bd0) repeat-x!important;border-color:#510bc4;color:#ececec}.dark-mode .bg-gradient-indigo.btn.active,.dark-mode .bg-gradient-indigo.btn:active,.dark-mode .bg-gradient-indigo.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-indigo.btn:not(:disabled):not(.disabled):active{background:#6610f2 linear-gradient(180deg,#6b2fcd,#510bc4) repeat-x!important;border-color:#4c0ab8;color:#fff}.dark-mode .bg-gradient-purple{background:#6f42c1 linear-gradient(180deg,#855eca,#6f42c1) repeat-x!important;color:#fff}.dark-mode .bg-gradient-purple.btn.disabled,.dark-mode .bg-gradient-purple.btn:disabled,.dark-mode .bg-gradient-purple.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-purple.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-purple.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-purple.btn:hover{background:#6f42c1 linear-gradient(180deg,#7655b4,#5e37a6) repeat-x!important;border-color:#59339d;color:#ececec}.dark-mode .bg-gradient-purple.btn.active,.dark-mode .bg-gradient-purple.btn:active,.dark-mode .bg-gradient-purple.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-purple.btn:not(:disabled):not(.disabled):active{background:#6f42c1 linear-gradient(180deg,#7252ab,#59339d) repeat-x!important;border-color:#533093;color:#fff}.dark-mode .bg-gradient-pink{background:#e83e8c linear-gradient(180deg,#eb5b9d,#e83e8c) repeat-x!important;color:#fff}.dark-mode .bg-gradient-pink.btn.disabled,.dark-mode .bg-gradient-pink.btn:disabled,.dark-mode .bg-gradient-pink.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-pink.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-pink.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-pink.btn:hover{background:#e83e8c linear-gradient(180deg,#e83e8c,#e41c78) repeat-x!important;border-color:#d91a72;color:#ececec}.dark-mode .bg-gradient-pink.btn.active,.dark-mode .bg-gradient-pink.btn:active,.dark-mode .bg-gradient-pink.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-pink.btn:not(:disabled):not(.disabled):active{background:#e83e8c linear-gradient(180deg,#df3c87,#d91a72) repeat-x!important;border-color:#ce196c;color:#fff}.dark-mode .bg-gradient-red{background:#e74c3c linear-gradient(180deg,#eb6759,#e74c3c) repeat-x!important;color:#fff}.dark-mode .bg-gradient-red.btn.disabled,.dark-mode .bg-gradient-red.btn:disabled,.dark-mode .bg-gradient-red.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-red.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-red.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-red.btn:hover{background:#e74c3c linear-gradient(180deg,#e64d3e,#e12e1c) repeat-x!important;border-color:#d62c1a;color:#ececec}.dark-mode .bg-gradient-red.btn.active,.dark-mode .bg-gradient-red.btn:active,.dark-mode .bg-gradient-red.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-red.btn:not(:disabled):not(.disabled):active{background:#e74c3c linear-gradient(180deg,#dc4c3d,#d62c1a) repeat-x!important;border-color:#ca2a19;color:#fff}.dark-mode .bg-gradient-orange{background:#fd7e14 linear-gradient(180deg,#fd9137,#fd7e14) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-orange.btn.disabled,.dark-mode .bg-gradient-orange.btn:disabled,.dark-mode .bg-gradient-orange.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-orange.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-orange.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-orange.btn:hover{background:#fd7e14 linear-gradient(180deg,#ec8128,#e96b02) repeat-x!important;border-color:#dc6502;color:#121a24}.dark-mode .bg-gradient-orange.btn.active,.dark-mode .bg-gradient-orange.btn:active,.dark-mode .bg-gradient-orange.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-orange.btn:not(:disabled):not(.disabled):active{background:#fd7e14 linear-gradient(180deg,#e17c28,#dc6502) repeat-x!important;border-color:#cf5f02;color:#fff}.dark-mode .bg-gradient-yellow{background:#f39c12 linear-gradient(180deg,#f5ab36,#f39c12) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-yellow.btn.disabled,.dark-mode .bg-gradient-yellow.btn:disabled,.dark-mode .bg-gradient-yellow.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-yellow.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-yellow.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-yellow.btn:hover{background:#f39c12 linear-gradient(180deg,#da982f,#d4860b) repeat-x!important;border-color:#c87f0a;color:#121a24}.dark-mode .bg-gradient-yellow.btn.active,.dark-mode .bg-gradient-yellow.btn:active,.dark-mode .bg-gradient-yellow.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-yellow.btn:not(:disabled):not(.disabled):active{background:#f39c12 linear-gradient(180deg,#d0922f,#c87f0a) repeat-x!important;border-color:#bc770a;color:#fff}.dark-mode .bg-gradient-green{background:#00bc8c linear-gradient(180deg,#26c69d,#00bc8c) repeat-x!important;color:#fff}.dark-mode .bg-gradient-green.btn.disabled,.dark-mode .bg-gradient-green.btn:disabled,.dark-mode .bg-gradient-green.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-green.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-green.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-green.btn:hover{background:#00bc8c linear-gradient(180deg,#26a685,#009670) repeat-x!important;border-color:#008966;color:#ececec}.dark-mode .bg-gradient-green.btn.active,.dark-mode .bg-gradient-green.btn:active,.dark-mode .bg-gradient-green.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-green.btn:not(:disabled):not(.disabled):active{background:#00bc8c linear-gradient(180deg,#269b7d,#008966) repeat-x!important;border-color:#007c5d;color:#fff}.dark-mode .bg-gradient-teal{background:#20c997 linear-gradient(180deg,#41d1a7,#20c997) repeat-x!important;color:#fff}.dark-mode .bg-gradient-teal.btn.disabled,.dark-mode .bg-gradient-teal.btn:disabled,.dark-mode .bg-gradient-teal.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-teal.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-teal.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-teal.btn:hover{background:#20c997 linear-gradient(180deg,#3db592,#1ba87e) repeat-x!important;border-color:#199d76;color:#ececec}.dark-mode .bg-gradient-teal.btn.active,.dark-mode .bg-gradient-teal.btn:active,.dark-mode .bg-gradient-teal.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-teal.btn:not(:disabled):not(.disabled):active{background:#20c997 linear-gradient(180deg,#3bac8b,#199d76) repeat-x!important;border-color:#17926e;color:#fff}.dark-mode .bg-gradient-cyan{background:#3498db linear-gradient(180deg,#52a7e0,#3498db) repeat-x!important;color:#fff}.dark-mode .bg-gradient-cyan.btn.disabled,.dark-mode .bg-gradient-cyan.btn:disabled,.dark-mode .bg-gradient-cyan.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-cyan.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-cyan.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-cyan.btn:hover{background:#3498db linear-gradient(180deg,#4497ce,#2384c6) repeat-x!important;border-color:#217dbb;color:#ececec}.dark-mode .bg-gradient-cyan.btn.active,.dark-mode .bg-gradient-cyan.btn:active,.dark-mode .bg-gradient-cyan.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-cyan.btn:not(:disabled):not(.disabled):active{background:#3498db linear-gradient(180deg,#4291c5,#217dbb) repeat-x!important;border-color:#1f76b0;color:#fff}.dark-mode .bg-gradient-white{background:#fff linear-gradient(180deg,#fff,#fff) repeat-x!important;color:#1f2d3d}.dark-mode .bg-gradient-white.btn.disabled,.dark-mode .bg-gradient-white.btn:disabled,.dark-mode .bg-gradient-white.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-white.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-white.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-white.btn:hover{background:#fff linear-gradient(180deg,#efefef,#ececec) repeat-x!important;border-color:#e6e6e6;color:#121a24}.dark-mode .bg-gradient-white.btn.active,.dark-mode .bg-gradient-white.btn:active,.dark-mode .bg-gradient-white.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-white.btn:not(:disabled):not(.disabled):active{background:#fff linear-gradient(180deg,#e9e9e9,#e6e6e6) repeat-x!important;border-color:#dfdfdf;color:#1f2d3d}.dark-mode .bg-gradient-gray{background:#6c757d linear-gradient(180deg,#828a91,#6c757d) repeat-x!important;color:#fff}.dark-mode .bg-gradient-gray.btn.disabled,.dark-mode .bg-gradient-gray.btn:disabled,.dark-mode .bg-gradient-gray.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-gray.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-gray.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-gray.btn:hover{background:#6c757d linear-gradient(180deg,#73797f,#5a6268) repeat-x!important;border-color:#545b62;color:#ececec}.dark-mode .bg-gradient-gray.btn.active,.dark-mode .bg-gradient-gray.btn:active,.dark-mode .bg-gradient-gray.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-gray.btn:not(:disabled):not(.disabled):active{background:#6c757d linear-gradient(180deg,#6e7479,#545b62) repeat-x!important;border-color:#4e555b;color:#fff}.dark-mode .bg-gradient-gray-dark{background:#343a40 linear-gradient(180deg,#52585d,#343a40) repeat-x!important;color:#fff}.dark-mode .bg-gradient-gray-dark.btn.disabled,.dark-mode .bg-gradient-gray-dark.btn:disabled,.dark-mode .bg-gradient-gray-dark.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-gray-dark.btn:not(:disabled):not(.disabled):active,.show>.dark-mode .bg-gradient-gray-dark.btn.dropdown-toggle{background-image:none!important}.dark-mode .bg-gradient-gray-dark.btn:hover{background:#343a40 linear-gradient(180deg,#44474b,#23272b) repeat-x!important;border-color:#1d2124;color:#ececec}.dark-mode .bg-gradient-gray-dark.btn.active,.dark-mode .bg-gradient-gray-dark.btn:active,.dark-mode .bg-gradient-gray-dark.btn:not(:disabled):not(.disabled).active,.dark-mode .bg-gradient-gray-dark.btn:not(:disabled):not(.disabled):active{background:#343a40 linear-gradient(180deg,#3f4245,#1d2124) repeat-x!important;border-color:#171a1d;color:#fff}.dark-mode .accent-primary .btn-link,.dark-mode .accent-primary .nav-tabs .nav-link,.dark-mode .accent-primary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#3f6791}.dark-mode .accent-primary .btn-link:hover,.dark-mode .accent-primary .nav-tabs .nav-link:hover,.dark-mode .accent-primary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#28415c}.dark-mode .accent-primary .dropdown-item.active,.dark-mode .accent-primary .dropdown-item:active{background-color:#3f6791;color:#fff}.dark-mode .accent-primary .custom-control-input:checked~.custom-control-label::before{background-color:#3f6791;border-color:#20344a}.dark-mode .accent-primary .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-primary .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-primary .custom-file-input:focus~.custom-file-label,.dark-mode .accent-primary .custom-select:focus,.dark-mode .accent-primary .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#85a7ca}.dark-mode .accent-primary .page-item .page-link{color:#3f6791}.dark-mode .accent-primary .page-item.active .page-link,.dark-mode .accent-primary .page-item.active a{background-color:#3f6791;border-color:#3f6791;color:#fff}.dark-mode .accent-primary .page-item.disabled .page-link,.dark-mode .accent-primary .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-primary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-primary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-primary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-primary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-primary .page-item .page-link:focus,.dark-mode .dark-mode.accent-primary .page-item .page-link:hover{color:#4774a3}.dark-mode .accent-secondary .btn-link,.dark-mode .accent-secondary .nav-tabs .nav-link,.dark-mode .accent-secondary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6c757d}.dark-mode .accent-secondary .btn-link:hover,.dark-mode .accent-secondary .nav-tabs .nav-link:hover,.dark-mode .accent-secondary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#494f54}.dark-mode .accent-secondary .dropdown-item.active,.dark-mode .accent-secondary .dropdown-item:active{background-color:#6c757d;color:#fff}.dark-mode .accent-secondary .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.dark-mode .accent-secondary .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-secondary .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-secondary .custom-file-input:focus~.custom-file-label,.dark-mode .accent-secondary .custom-select:focus,.dark-mode .accent-secondary .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#afb5ba}.dark-mode .accent-secondary .page-item .page-link{color:#6c757d}.dark-mode .accent-secondary .page-item.active .page-link,.dark-mode .accent-secondary .page-item.active a{background-color:#6c757d;border-color:#6c757d;color:#fff}.dark-mode .accent-secondary .page-item.disabled .page-link,.dark-mode .accent-secondary .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-secondary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-secondary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-secondary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-secondary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-secondary .page-item .page-link:focus,.dark-mode .dark-mode.accent-secondary .page-item .page-link:hover{color:#78828a}.dark-mode .accent-success .btn-link,.dark-mode .accent-success .nav-tabs .nav-link,.dark-mode .accent-success a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#00bc8c}.dark-mode .accent-success .btn-link:hover,.dark-mode .accent-success .nav-tabs .nav-link:hover,.dark-mode .accent-success a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#007053}.dark-mode .accent-success .dropdown-item.active,.dark-mode .accent-success .dropdown-item:active{background-color:#00bc8c;color:#fff}.dark-mode .accent-success .custom-control-input:checked~.custom-control-label::before{background-color:#00bc8c;border-color:#005640}.dark-mode .accent-success .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-success .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-success .custom-file-input:focus~.custom-file-label,.dark-mode .accent-success .custom-select:focus,.dark-mode .accent-success .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#3dffcd}.dark-mode .accent-success .page-item .page-link{color:#00bc8c}.dark-mode .accent-success .page-item.active .page-link,.dark-mode .accent-success .page-item.active a{background-color:#00bc8c;border-color:#00bc8c;color:#fff}.dark-mode .accent-success .page-item.disabled .page-link,.dark-mode .accent-success .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-success [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-success [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-success [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-success [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-success .page-item .page-link:focus,.dark-mode .dark-mode.accent-success .page-item .page-link:hover{color:#00d69f}.dark-mode .accent-info .btn-link,.dark-mode .accent-info .nav-tabs .nav-link,.dark-mode .accent-info a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#3498db}.dark-mode .accent-info .btn-link:hover,.dark-mode .accent-info .nav-tabs .nav-link:hover,.dark-mode .accent-info a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#1d6fa5}.dark-mode .accent-info .dropdown-item.active,.dark-mode .accent-info .dropdown-item:active{background-color:#3498db;color:#fff}.dark-mode .accent-info .custom-control-input:checked~.custom-control-label::before{background-color:#3498db;border-color:#196090}.dark-mode .accent-info .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-info .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-info .custom-file-input:focus~.custom-file-label,.dark-mode .accent-info .custom-select:focus,.dark-mode .accent-info .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#a0cfee}.dark-mode .accent-info .page-item .page-link{color:#3498db}.dark-mode .accent-info .page-item.active .page-link,.dark-mode .accent-info .page-item.active a{background-color:#3498db;border-color:#3498db;color:#fff}.dark-mode .accent-info .page-item.disabled .page-link,.dark-mode .accent-info .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-info [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-info [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-info [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-info [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-info .page-item .page-link:focus,.dark-mode .dark-mode.accent-info .page-item .page-link:hover{color:#4aa3df}.dark-mode .accent-warning .btn-link,.dark-mode .accent-warning .nav-tabs .nav-link,.dark-mode .accent-warning a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#f39c12}.dark-mode .accent-warning .btn-link:hover,.dark-mode .accent-warning .nav-tabs .nav-link:hover,.dark-mode .accent-warning a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#b06f09}.dark-mode .accent-warning .dropdown-item.active,.dark-mode .accent-warning .dropdown-item:active{background-color:#f39c12;color:#1f2d3d}.dark-mode .accent-warning .custom-control-input:checked~.custom-control-label::before{background-color:#f39c12;border-color:#976008}.dark-mode .accent-warning .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-warning .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-warning .custom-file-input:focus~.custom-file-label,.dark-mode .accent-warning .custom-select:focus,.dark-mode .accent-warning .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f9cf8b}.dark-mode .accent-warning .page-item .page-link{color:#f39c12}.dark-mode .accent-warning .page-item.active .page-link,.dark-mode .accent-warning .page-item.active a{background-color:#f39c12;border-color:#f39c12;color:#fff}.dark-mode .accent-warning .page-item.disabled .page-link,.dark-mode .accent-warning .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-warning [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-warning [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-warning [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-warning [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-warning .page-item .page-link:focus,.dark-mode .dark-mode.accent-warning .page-item .page-link:hover{color:#f4a62a}.dark-mode .accent-danger .btn-link,.dark-mode .accent-danger .nav-tabs .nav-link,.dark-mode .accent-danger a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#e74c3c}.dark-mode .accent-danger .btn-link:hover,.dark-mode .accent-danger .nav-tabs .nav-link:hover,.dark-mode .accent-danger a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#bf2718}.dark-mode .accent-danger .dropdown-item.active,.dark-mode .accent-danger .dropdown-item:active{background-color:#e74c3c;color:#fff}.dark-mode .accent-danger .custom-control-input:checked~.custom-control-label::before{background-color:#e74c3c;border-color:#a82315}.dark-mode .accent-danger .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-danger .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-danger .custom-file-input:focus~.custom-file-label,.dark-mode .accent-danger .custom-select:focus,.dark-mode .accent-danger .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f5b4ae}.dark-mode .accent-danger .page-item .page-link{color:#e74c3c}.dark-mode .accent-danger .page-item.active .page-link,.dark-mode .accent-danger .page-item.active a{background-color:#e74c3c;border-color:#e74c3c;color:#fff}.dark-mode .accent-danger .page-item.disabled .page-link,.dark-mode .accent-danger .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-danger [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-danger [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-danger [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-danger [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-danger .page-item .page-link:focus,.dark-mode .dark-mode.accent-danger .page-item .page-link:hover{color:#ea6153}.dark-mode .accent-light .btn-link,.dark-mode .accent-light .nav-tabs .nav-link,.dark-mode .accent-light a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#f8f9fa}.dark-mode .accent-light .btn-link:hover,.dark-mode .accent-light .nav-tabs .nav-link:hover,.dark-mode .accent-light a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#cbd3da}.dark-mode .accent-light .dropdown-item.active,.dark-mode .accent-light .dropdown-item:active{background-color:#f8f9fa;color:#1f2d3d}.dark-mode .accent-light .custom-control-input:checked~.custom-control-label::before{background-color:#f8f9fa;border-color:#bdc6d0}.dark-mode .accent-light .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-light .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-light .custom-file-input:focus~.custom-file-label,.dark-mode .accent-light .custom-select:focus,.dark-mode .accent-light .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fff}.dark-mode .accent-light .page-item .page-link{color:#f8f9fa}.dark-mode .accent-light .page-item.active .page-link,.dark-mode .accent-light .page-item.active a{background-color:#f8f9fa;border-color:#f8f9fa;color:#fff}.dark-mode .accent-light .page-item.disabled .page-link,.dark-mode .accent-light .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-light [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-light [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-light [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-light [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-light .page-item .page-link:focus,.dark-mode .dark-mode.accent-light .page-item .page-link:hover{color:#fff}.dark-mode .accent-dark .btn-link,.dark-mode .accent-dark .nav-tabs .nav-link,.dark-mode .accent-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#343a40}.dark-mode .accent-dark .btn-link:hover,.dark-mode .accent-dark .nav-tabs .nav-link:hover,.dark-mode .accent-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#121416}.dark-mode .accent-dark .dropdown-item.active,.dark-mode .accent-dark .dropdown-item:active{background-color:#343a40;color:#fff}.dark-mode .accent-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.dark-mode .accent-dark .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-dark .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-dark .custom-file-input:focus~.custom-file-label,.dark-mode .accent-dark .custom-select:focus,.dark-mode .accent-dark .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#6d7a86}.dark-mode .accent-dark .page-item .page-link{color:#343a40}.dark-mode .accent-dark .page-item.active .page-link,.dark-mode .accent-dark .page-item.active a{background-color:#343a40;border-color:#343a40;color:#fff}.dark-mode .accent-dark .page-item.disabled .page-link,.dark-mode .accent-dark .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-dark .page-item .page-link:focus,.dark-mode .dark-mode.accent-dark .page-item .page-link:hover{color:#3f474e}.dark-mode [class*=accent-] a.btn-primary{color:#fff}.dark-mode [class*=accent-] a.btn-secondary{color:#fff}.dark-mode [class*=accent-] a.btn-success{color:#fff}.dark-mode [class*=accent-] a.btn-info{color:#fff}.dark-mode [class*=accent-] a.btn-warning{color:#1f2d3d}.dark-mode [class*=accent-] a.btn-danger{color:#fff}.dark-mode [class*=accent-] a.btn-light{color:#1f2d3d}.dark-mode [class*=accent-] a.btn-dark{color:#fff}.dark-mode .accent-lightblue .btn-link,.dark-mode .accent-lightblue .nav-tabs .nav-link,.dark-mode .accent-lightblue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#86bad8}.dark-mode .accent-lightblue .btn-link:hover,.dark-mode .accent-lightblue .nav-tabs .nav-link:hover,.dark-mode .accent-lightblue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#4c99c6}.dark-mode .accent-lightblue .dropdown-item.active,.dark-mode .accent-lightblue .dropdown-item:active{background-color:#86bad8;color:#1f2d3d}.dark-mode .accent-lightblue .custom-control-input:checked~.custom-control-label::before{background-color:#86bad8;border-color:#3c8dbc}.dark-mode .accent-lightblue .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-lightblue .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-lightblue .custom-file-input:focus~.custom-file-label,.dark-mode .accent-lightblue .custom-select:focus,.dark-mode .accent-lightblue .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#e6f1f7}.dark-mode .accent-lightblue .page-item .page-link{color:#86bad8}.dark-mode .accent-lightblue .page-item.active .page-link,.dark-mode .accent-lightblue .page-item.active a{background-color:#86bad8;border-color:#86bad8;color:#fff}.dark-mode .accent-lightblue .page-item.disabled .page-link,.dark-mode .accent-lightblue .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-lightblue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-lightblue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-lightblue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-lightblue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-lightblue .page-item .page-link:focus,.dark-mode .dark-mode.accent-lightblue .page-item .page-link:hover{color:#99c5de}.dark-mode .accent-navy .btn-link,.dark-mode .accent-navy .nav-tabs .nav-link,.dark-mode .accent-navy a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#002c59}.dark-mode .accent-navy .btn-link:hover,.dark-mode .accent-navy .nav-tabs .nav-link:hover,.dark-mode .accent-navy a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#00060c}.dark-mode .accent-navy .dropdown-item.active,.dark-mode .accent-navy .dropdown-item:active{background-color:#002c59;color:#fff}.dark-mode .accent-navy .custom-control-input:checked~.custom-control-label::before{background-color:#002c59;border-color:#000}.dark-mode .accent-navy .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-navy .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-navy .custom-file-input:focus~.custom-file-label,.dark-mode .accent-navy .custom-select:focus,.dark-mode .accent-navy .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#006ad8}.dark-mode .accent-navy .page-item .page-link{color:#002c59}.dark-mode .accent-navy .page-item.active .page-link,.dark-mode .accent-navy .page-item.active a{background-color:#002c59;border-color:#002c59;color:#fff}.dark-mode .accent-navy .page-item.disabled .page-link,.dark-mode .accent-navy .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-navy [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-navy [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-navy [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-navy [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-navy .page-item .page-link:focus,.dark-mode .dark-mode.accent-navy .page-item .page-link:hover{color:#003872}.dark-mode .accent-olive .btn-link,.dark-mode .accent-olive .nav-tabs .nav-link,.dark-mode .accent-olive a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#74c8a3}.dark-mode .accent-olive .btn-link:hover,.dark-mode .accent-olive .nav-tabs .nav-link:hover,.dark-mode .accent-olive a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#44ab7d}.dark-mode .accent-olive .dropdown-item.active,.dark-mode .accent-olive .dropdown-item:active{background-color:#74c8a3;color:#1f2d3d}.dark-mode .accent-olive .custom-control-input:checked~.custom-control-label::before{background-color:#74c8a3;border-color:#3d9970}.dark-mode .accent-olive .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-olive .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-olive .custom-file-input:focus~.custom-file-label,.dark-mode .accent-olive .custom-select:focus,.dark-mode .accent-olive .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#cfecdf}.dark-mode .accent-olive .page-item .page-link{color:#74c8a3}.dark-mode .accent-olive .page-item.active .page-link,.dark-mode .accent-olive .page-item.active a{background-color:#74c8a3;border-color:#74c8a3;color:#fff}.dark-mode .accent-olive .page-item.disabled .page-link,.dark-mode .accent-olive .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-olive [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-olive [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-olive [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-olive [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-olive .page-item .page-link:focus,.dark-mode .dark-mode.accent-olive .page-item .page-link:hover{color:#87cfaf}.dark-mode .accent-lime .btn-link,.dark-mode .accent-lime .nav-tabs .nav-link,.dark-mode .accent-lime a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#67ffa9}.dark-mode .accent-lime .btn-link:hover,.dark-mode .accent-lime .nav-tabs .nav-link:hover,.dark-mode .accent-lime a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#1bff7e}.dark-mode .accent-lime .dropdown-item.active,.dark-mode .accent-lime .dropdown-item:active{background-color:#67ffa9;color:#1f2d3d}.dark-mode .accent-lime .custom-control-input:checked~.custom-control-label::before{background-color:#67ffa9;border-color:#01ff70}.dark-mode .accent-lime .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-lime .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-lime .custom-file-input:focus~.custom-file-label,.dark-mode .accent-lime .custom-select:focus,.dark-mode .accent-lime .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#e7fff1}.dark-mode .accent-lime .page-item .page-link{color:#67ffa9}.dark-mode .accent-lime .page-item.active .page-link,.dark-mode .accent-lime .page-item.active a{background-color:#67ffa9;border-color:#67ffa9;color:#fff}.dark-mode .accent-lime .page-item.disabled .page-link,.dark-mode .accent-lime .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-lime [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-lime [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-lime [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-lime [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-lime .page-item .page-link:focus,.dark-mode .dark-mode.accent-lime .page-item .page-link:hover{color:#81ffb8}.dark-mode .accent-fuchsia .btn-link,.dark-mode .accent-fuchsia .nav-tabs .nav-link,.dark-mode .accent-fuchsia a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#f672d8}.dark-mode .accent-fuchsia .btn-link:hover,.dark-mode .accent-fuchsia .nav-tabs .nav-link:hover,.dark-mode .accent-fuchsia a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#f22ac5}.dark-mode .accent-fuchsia .dropdown-item.active,.dark-mode .accent-fuchsia .dropdown-item:active{background-color:#f672d8;color:#1f2d3d}.dark-mode .accent-fuchsia .custom-control-input:checked~.custom-control-label::before{background-color:#f672d8;border-color:#f012be}.dark-mode .accent-fuchsia .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-fuchsia .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-fuchsia .custom-file-input:focus~.custom-file-label,.dark-mode .accent-fuchsia .custom-select:focus,.dark-mode .accent-fuchsia .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#feeaf9}.dark-mode .accent-fuchsia .page-item .page-link{color:#f672d8}.dark-mode .accent-fuchsia .page-item.active .page-link,.dark-mode .accent-fuchsia .page-item.active a{background-color:#f672d8;border-color:#f672d8;color:#fff}.dark-mode .accent-fuchsia .page-item.disabled .page-link,.dark-mode .accent-fuchsia .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-fuchsia [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-fuchsia [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-fuchsia [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-fuchsia [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-fuchsia .page-item .page-link:focus,.dark-mode .dark-mode.accent-fuchsia .page-item .page-link:hover{color:#f88adf}.dark-mode .accent-maroon .btn-link,.dark-mode .accent-maroon .nav-tabs .nav-link,.dark-mode .accent-maroon a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#ed6c9b}.dark-mode .accent-maroon .btn-link:hover,.dark-mode .accent-maroon .nav-tabs .nav-link:hover,.dark-mode .accent-maroon a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#e4286d}.dark-mode .accent-maroon .dropdown-item.active,.dark-mode .accent-maroon .dropdown-item:active{background-color:#ed6c9b;color:#1f2d3d}.dark-mode .accent-maroon .custom-control-input:checked~.custom-control-label::before{background-color:#ed6c9b;border-color:#d81b60}.dark-mode .accent-maroon .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-maroon .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-maroon .custom-file-input:focus~.custom-file-label,.dark-mode .accent-maroon .custom-select:focus,.dark-mode .accent-maroon .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fbdee8}.dark-mode .accent-maroon .page-item .page-link{color:#ed6c9b}.dark-mode .accent-maroon .page-item.active .page-link,.dark-mode .accent-maroon .page-item.active a{background-color:#ed6c9b;border-color:#ed6c9b;color:#fff}.dark-mode .accent-maroon .page-item.disabled .page-link,.dark-mode .accent-maroon .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-maroon [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-maroon [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-maroon [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-maroon [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-maroon .page-item .page-link:focus,.dark-mode .dark-mode.accent-maroon .page-item .page-link:hover{color:#f083ab}.dark-mode .accent-blue .btn-link,.dark-mode .accent-blue .nav-tabs .nav-link,.dark-mode .accent-blue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#3f6791}.dark-mode .accent-blue .btn-link:hover,.dark-mode .accent-blue .nav-tabs .nav-link:hover,.dark-mode .accent-blue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#28415c}.dark-mode .accent-blue .dropdown-item.active,.dark-mode .accent-blue .dropdown-item:active{background-color:#3f6791;color:#fff}.dark-mode .accent-blue .custom-control-input:checked~.custom-control-label::before{background-color:#3f6791;border-color:#20344a}.dark-mode .accent-blue .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-blue .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-blue .custom-file-input:focus~.custom-file-label,.dark-mode .accent-blue .custom-select:focus,.dark-mode .accent-blue .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#85a7ca}.dark-mode .accent-blue .page-item .page-link{color:#3f6791}.dark-mode .accent-blue .page-item.active .page-link,.dark-mode .accent-blue .page-item.active a{background-color:#3f6791;border-color:#3f6791;color:#fff}.dark-mode .accent-blue .page-item.disabled .page-link,.dark-mode .accent-blue .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-blue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-blue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-blue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-blue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-blue .page-item .page-link:focus,.dark-mode .dark-mode.accent-blue .page-item .page-link:hover{color:#4774a3}.dark-mode .accent-indigo .btn-link,.dark-mode .accent-indigo .nav-tabs .nav-link,.dark-mode .accent-indigo a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6610f2}.dark-mode .accent-indigo .btn-link:hover,.dark-mode .accent-indigo .nav-tabs .nav-link:hover,.dark-mode .accent-indigo a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#4709ac}.dark-mode .accent-indigo .dropdown-item.active,.dark-mode .accent-indigo .dropdown-item:active{background-color:#6610f2;color:#fff}.dark-mode .accent-indigo .custom-control-input:checked~.custom-control-label::before{background-color:#6610f2;border-color:#3d0894}.dark-mode .accent-indigo .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-indigo .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-indigo .custom-file-input:focus~.custom-file-label,.dark-mode .accent-indigo .custom-select:focus,.dark-mode .accent-indigo .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#b389f9}.dark-mode .accent-indigo .page-item .page-link{color:#6610f2}.dark-mode .accent-indigo .page-item.active .page-link,.dark-mode .accent-indigo .page-item.active a{background-color:#6610f2;border-color:#6610f2;color:#fff}.dark-mode .accent-indigo .page-item.disabled .page-link,.dark-mode .accent-indigo .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-indigo [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-indigo [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-indigo [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-indigo [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-indigo .page-item .page-link:focus,.dark-mode .dark-mode.accent-indigo .page-item .page-link:hover{color:#7528f3}.dark-mode .accent-purple .btn-link,.dark-mode .accent-purple .nav-tabs .nav-link,.dark-mode .accent-purple a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6f42c1}.dark-mode .accent-purple .btn-link:hover,.dark-mode .accent-purple .nav-tabs .nav-link:hover,.dark-mode .accent-purple a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#4e2d89}.dark-mode .accent-purple .dropdown-item.active,.dark-mode .accent-purple .dropdown-item:active{background-color:#6f42c1;color:#fff}.dark-mode .accent-purple .custom-control-input:checked~.custom-control-label::before{background-color:#6f42c1;border-color:#432776}.dark-mode .accent-purple .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-purple .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-purple .custom-file-input:focus~.custom-file-label,.dark-mode .accent-purple .custom-select:focus,.dark-mode .accent-purple .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#b8a2e0}.dark-mode .accent-purple .page-item .page-link{color:#6f42c1}.dark-mode .accent-purple .page-item.active .page-link,.dark-mode .accent-purple .page-item.active a{background-color:#6f42c1;border-color:#6f42c1;color:#fff}.dark-mode .accent-purple .page-item.disabled .page-link,.dark-mode .accent-purple .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-purple [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-purple [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-purple [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-purple [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-purple .page-item .page-link:focus,.dark-mode .dark-mode.accent-purple .page-item .page-link:hover{color:#7e55c7}.dark-mode .accent-pink .btn-link,.dark-mode .accent-pink .nav-tabs .nav-link,.dark-mode .accent-pink a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#e83e8c}.dark-mode .accent-pink .btn-link:hover,.dark-mode .accent-pink .nav-tabs .nav-link:hover,.dark-mode .accent-pink a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#c21766}.dark-mode .accent-pink .dropdown-item.active,.dark-mode .accent-pink .dropdown-item:active{background-color:#e83e8c;color:#fff}.dark-mode .accent-pink .custom-control-input:checked~.custom-control-label::before{background-color:#e83e8c;border-color:#ac145a}.dark-mode .accent-pink .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-pink .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-pink .custom-file-input:focus~.custom-file-label,.dark-mode .accent-pink .custom-select:focus,.dark-mode .accent-pink .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f6b0d0}.dark-mode .accent-pink .page-item .page-link{color:#e83e8c}.dark-mode .accent-pink .page-item.active .page-link,.dark-mode .accent-pink .page-item.active a{background-color:#e83e8c;border-color:#e83e8c;color:#fff}.dark-mode .accent-pink .page-item.disabled .page-link,.dark-mode .accent-pink .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-pink [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-pink [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-pink [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-pink [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-pink .page-item .page-link:focus,.dark-mode .dark-mode.accent-pink .page-item .page-link:hover{color:#eb559a}.dark-mode .accent-red .btn-link,.dark-mode .accent-red .nav-tabs .nav-link,.dark-mode .accent-red a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#e74c3c}.dark-mode .accent-red .btn-link:hover,.dark-mode .accent-red .nav-tabs .nav-link:hover,.dark-mode .accent-red a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#bf2718}.dark-mode .accent-red .dropdown-item.active,.dark-mode .accent-red .dropdown-item:active{background-color:#e74c3c;color:#fff}.dark-mode .accent-red .custom-control-input:checked~.custom-control-label::before{background-color:#e74c3c;border-color:#a82315}.dark-mode .accent-red .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-red .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-red .custom-file-input:focus~.custom-file-label,.dark-mode .accent-red .custom-select:focus,.dark-mode .accent-red .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f5b4ae}.dark-mode .accent-red .page-item .page-link{color:#e74c3c}.dark-mode .accent-red .page-item.active .page-link,.dark-mode .accent-red .page-item.active a{background-color:#e74c3c;border-color:#e74c3c;color:#fff}.dark-mode .accent-red .page-item.disabled .page-link,.dark-mode .accent-red .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-red [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-red [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-red [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-red [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-red .page-item .page-link:focus,.dark-mode .dark-mode.accent-red .page-item .page-link:hover{color:#ea6153}.dark-mode .accent-orange .btn-link,.dark-mode .accent-orange .nav-tabs .nav-link,.dark-mode .accent-orange a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#fd7e14}.dark-mode .accent-orange .btn-link:hover,.dark-mode .accent-orange .nav-tabs .nav-link:hover,.dark-mode .accent-orange a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#c35a02}.dark-mode .accent-orange .dropdown-item.active,.dark-mode .accent-orange .dropdown-item:active{background-color:#fd7e14;color:#1f2d3d}.dark-mode .accent-orange .custom-control-input:checked~.custom-control-label::before{background-color:#fd7e14;border-color:#aa4e01}.dark-mode .accent-orange .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-orange .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-orange .custom-file-input:focus~.custom-file-label,.dark-mode .accent-orange .custom-select:focus,.dark-mode .accent-orange .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fec392}.dark-mode .accent-orange .page-item .page-link{color:#fd7e14}.dark-mode .accent-orange .page-item.active .page-link,.dark-mode .accent-orange .page-item.active a{background-color:#fd7e14;border-color:#fd7e14;color:#fff}.dark-mode .accent-orange .page-item.disabled .page-link,.dark-mode .accent-orange .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-orange [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-orange [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-orange [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-orange [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-orange .page-item .page-link:focus,.dark-mode .dark-mode.accent-orange .page-item .page-link:hover{color:#fd8c2d}.dark-mode .accent-yellow .btn-link,.dark-mode .accent-yellow .nav-tabs .nav-link,.dark-mode .accent-yellow a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#f39c12}.dark-mode .accent-yellow .btn-link:hover,.dark-mode .accent-yellow .nav-tabs .nav-link:hover,.dark-mode .accent-yellow a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#b06f09}.dark-mode .accent-yellow .dropdown-item.active,.dark-mode .accent-yellow .dropdown-item:active{background-color:#f39c12;color:#1f2d3d}.dark-mode .accent-yellow .custom-control-input:checked~.custom-control-label::before{background-color:#f39c12;border-color:#976008}.dark-mode .accent-yellow .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-yellow .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-yellow .custom-file-input:focus~.custom-file-label,.dark-mode .accent-yellow .custom-select:focus,.dark-mode .accent-yellow .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f9cf8b}.dark-mode .accent-yellow .page-item .page-link{color:#f39c12}.dark-mode .accent-yellow .page-item.active .page-link,.dark-mode .accent-yellow .page-item.active a{background-color:#f39c12;border-color:#f39c12;color:#fff}.dark-mode .accent-yellow .page-item.disabled .page-link,.dark-mode .accent-yellow .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-yellow [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-yellow [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-yellow [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-yellow [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-yellow .page-item .page-link:focus,.dark-mode .dark-mode.accent-yellow .page-item .page-link:hover{color:#f4a62a}.dark-mode .accent-green .btn-link,.dark-mode .accent-green .nav-tabs .nav-link,.dark-mode .accent-green a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#00bc8c}.dark-mode .accent-green .btn-link:hover,.dark-mode .accent-green .nav-tabs .nav-link:hover,.dark-mode .accent-green a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#007053}.dark-mode .accent-green .dropdown-item.active,.dark-mode .accent-green .dropdown-item:active{background-color:#00bc8c;color:#fff}.dark-mode .accent-green .custom-control-input:checked~.custom-control-label::before{background-color:#00bc8c;border-color:#005640}.dark-mode .accent-green .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-green .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-green .custom-file-input:focus~.custom-file-label,.dark-mode .accent-green .custom-select:focus,.dark-mode .accent-green .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#3dffcd}.dark-mode .accent-green .page-item .page-link{color:#00bc8c}.dark-mode .accent-green .page-item.active .page-link,.dark-mode .accent-green .page-item.active a{background-color:#00bc8c;border-color:#00bc8c;color:#fff}.dark-mode .accent-green .page-item.disabled .page-link,.dark-mode .accent-green .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-green [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-green [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-green [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-green [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-green .page-item .page-link:focus,.dark-mode .dark-mode.accent-green .page-item .page-link:hover{color:#00d69f}.dark-mode .accent-teal .btn-link,.dark-mode .accent-teal .nav-tabs .nav-link,.dark-mode .accent-teal a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#20c997}.dark-mode .accent-teal .btn-link:hover,.dark-mode .accent-teal .nav-tabs .nav-link:hover,.dark-mode .accent-teal a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#158765}.dark-mode .accent-teal .dropdown-item.active,.dark-mode .accent-teal .dropdown-item:active{background-color:#20c997;color:#fff}.dark-mode .accent-teal .custom-control-input:checked~.custom-control-label::before{background-color:#20c997;border-color:#127155}.dark-mode .accent-teal .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-teal .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-teal .custom-file-input:focus~.custom-file-label,.dark-mode .accent-teal .custom-select:focus,.dark-mode .accent-teal .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#7eeaca}.dark-mode .accent-teal .page-item .page-link{color:#20c997}.dark-mode .accent-teal .page-item.active .page-link,.dark-mode .accent-teal .page-item.active a{background-color:#20c997;border-color:#20c997;color:#fff}.dark-mode .accent-teal .page-item.disabled .page-link,.dark-mode .accent-teal .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-teal [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-teal [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-teal [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-teal [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-teal .page-item .page-link:focus,.dark-mode .dark-mode.accent-teal .page-item .page-link:hover{color:#26dca6}.dark-mode .accent-cyan .btn-link,.dark-mode .accent-cyan .nav-tabs .nav-link,.dark-mode .accent-cyan a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#3498db}.dark-mode .accent-cyan .btn-link:hover,.dark-mode .accent-cyan .nav-tabs .nav-link:hover,.dark-mode .accent-cyan a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#1d6fa5}.dark-mode .accent-cyan .dropdown-item.active,.dark-mode .accent-cyan .dropdown-item:active{background-color:#3498db;color:#fff}.dark-mode .accent-cyan .custom-control-input:checked~.custom-control-label::before{background-color:#3498db;border-color:#196090}.dark-mode .accent-cyan .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-cyan .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-cyan .custom-file-input:focus~.custom-file-label,.dark-mode .accent-cyan .custom-select:focus,.dark-mode .accent-cyan .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#a0cfee}.dark-mode .accent-cyan .page-item .page-link{color:#3498db}.dark-mode .accent-cyan .page-item.active .page-link,.dark-mode .accent-cyan .page-item.active a{background-color:#3498db;border-color:#3498db;color:#fff}.dark-mode .accent-cyan .page-item.disabled .page-link,.dark-mode .accent-cyan .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-cyan [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-cyan [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-cyan [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-cyan [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-cyan .page-item .page-link:focus,.dark-mode .dark-mode.accent-cyan .page-item .page-link:hover{color:#4aa3df}.dark-mode .accent-white .btn-link,.dark-mode .accent-white .nav-tabs .nav-link,.dark-mode .accent-white a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#fff}.dark-mode .accent-white .btn-link:hover,.dark-mode .accent-white .nav-tabs .nav-link:hover,.dark-mode .accent-white a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#d9d9d9}.dark-mode .accent-white .dropdown-item.active,.dark-mode .accent-white .dropdown-item:active{background-color:#fff;color:#1f2d3d}.dark-mode .accent-white .custom-control-input:checked~.custom-control-label::before{background-color:#fff;border-color:#ccc}.dark-mode .accent-white .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231f2d3d' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-white .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-white .custom-file-input:focus~.custom-file-label,.dark-mode .accent-white .custom-select:focus,.dark-mode .accent-white .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fff}.dark-mode .accent-white .page-item .page-link{color:#fff}.dark-mode .accent-white .page-item.active .page-link,.dark-mode .accent-white .page-item.active a{background-color:#fff;border-color:#fff;color:#fff}.dark-mode .accent-white .page-item.disabled .page-link,.dark-mode .accent-white .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-white [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-white [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-white [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-white [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-white .page-item .page-link:focus,.dark-mode .dark-mode.accent-white .page-item .page-link:hover{color:#fff}.dark-mode .accent-gray .btn-link,.dark-mode .accent-gray .nav-tabs .nav-link,.dark-mode .accent-gray a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#6c757d}.dark-mode .accent-gray .btn-link:hover,.dark-mode .accent-gray .nav-tabs .nav-link:hover,.dark-mode .accent-gray a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#494f54}.dark-mode .accent-gray .dropdown-item.active,.dark-mode .accent-gray .dropdown-item:active{background-color:#6c757d;color:#fff}.dark-mode .accent-gray .custom-control-input:checked~.custom-control-label::before{background-color:#6c757d;border-color:#3d4246}.dark-mode .accent-gray .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-gray .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-gray .custom-file-input:focus~.custom-file-label,.dark-mode .accent-gray .custom-select:focus,.dark-mode .accent-gray .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#afb5ba}.dark-mode .accent-gray .page-item .page-link{color:#6c757d}.dark-mode .accent-gray .page-item.active .page-link,.dark-mode .accent-gray .page-item.active a{background-color:#6c757d;border-color:#6c757d;color:#fff}.dark-mode .accent-gray .page-item.disabled .page-link,.dark-mode .accent-gray .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-gray [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-gray [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-gray [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-gray [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-gray .page-item .page-link:focus,.dark-mode .dark-mode.accent-gray .page-item .page-link:hover{color:#78828a}.dark-mode .accent-gray-dark .btn-link,.dark-mode .accent-gray-dark .nav-tabs .nav-link,.dark-mode .accent-gray-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn){color:#343a40}.dark-mode .accent-gray-dark .btn-link:hover,.dark-mode .accent-gray-dark .nav-tabs .nav-link:hover,.dark-mode .accent-gray-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):not(.btn):hover{color:#121416}.dark-mode .accent-gray-dark .dropdown-item.active,.dark-mode .accent-gray-dark .dropdown-item:active{background-color:#343a40;color:#fff}.dark-mode .accent-gray-dark .custom-control-input:checked~.custom-control-label::before{background-color:#343a40;border-color:#060708}.dark-mode .accent-gray-dark .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.dark-mode .accent-gray-dark .custom-control-input:focus:not(:checked)~.custom-control-label::before,.dark-mode .accent-gray-dark .custom-file-input:focus~.custom-file-label,.dark-mode .accent-gray-dark .custom-select:focus,.dark-mode .accent-gray-dark .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#6d7a86}.dark-mode .accent-gray-dark .page-item .page-link{color:#343a40}.dark-mode .accent-gray-dark .page-item.active .page-link,.dark-mode .accent-gray-dark .page-item.active a{background-color:#343a40;border-color:#343a40;color:#fff}.dark-mode .accent-gray-dark .page-item.disabled .page-link,.dark-mode .accent-gray-dark .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.dark-mode .accent-gray-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.dark-mode .accent-gray-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.dark-mode .accent-gray-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.dark-mode .accent-gray-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.dark-mode .dark-mode.accent-gray-dark .page-item .page-link:focus,.dark-mode .dark-mode.accent-gray-dark .page-item .page-link:hover{color:#3f474e} +/*# sourceMappingURL=adminlte.min.css.map */ \ No newline at end of file diff --git a/ems-core/web-admin/public/assets/img/circuit-board-5907811_1920-bw.png b/ems-core/web-admin/public/assets/img/circuit-board-5907811_1920-bw.png new file mode 100644 index 0000000..683e378 Binary files /dev/null and b/ems-core/web-admin/public/assets/img/circuit-board-5907811_1920-bw.png differ diff --git a/ems-core/web-admin/public/assets/img/ems-logo-192x192.png b/ems-core/web-admin/public/assets/img/ems-logo-192x192.png new file mode 100644 index 0000000..66bfa78 Binary files /dev/null and b/ems-core/web-admin/public/assets/img/ems-logo-192x192.png differ diff --git a/ems-core/web-admin/public/assets/img/wave-loader-green-sm.gif b/ems-core/web-admin/public/assets/img/wave-loader-green-sm.gif new file mode 100644 index 0000000..972aba6 Binary files /dev/null and b/ems-core/web-admin/public/assets/img/wave-loader-green-sm.gif differ diff --git a/ems-core/web-admin/public/assets/img/wave-loader-green.gif b/ems-core/web-admin/public/assets/img/wave-loader-green.gif new file mode 100644 index 0000000..2daf768 Binary files /dev/null and b/ems-core/web-admin/public/assets/img/wave-loader-green.gif differ diff --git a/ems-core/web-admin/public/assets/img/wave-loader-grey-sm.gif b/ems-core/web-admin/public/assets/img/wave-loader-grey-sm.gif new file mode 100644 index 0000000..dfa82dd Binary files /dev/null and b/ems-core/web-admin/public/assets/img/wave-loader-grey-sm.gif differ diff --git a/ems-core/web-admin/public/assets/img/wave-loader-grey.gif b/ems-core/web-admin/public/assets/img/wave-loader-grey.gif new file mode 100644 index 0000000..b1a0b89 Binary files /dev/null and b/ems-core/web-admin/public/assets/img/wave-loader-grey.gif differ diff --git a/ems-core/web-admin/public/assets/js/adminlte.js b/ems-core/web-admin/public/assets/js/adminlte.js new file mode 100644 index 0000000..f7cce66 --- /dev/null +++ b/ems-core/web-admin/public/assets/js/adminlte.js @@ -0,0 +1,2962 @@ +/*! + * AdminLTE v3.1.0 (https://adminlte.io) + * Copyright 2014-2021 Colorlib + * Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE) + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery')) : + typeof define === 'function' && define.amd ? define(['exports', 'jquery'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.adminlte = {}, global.jQuery)); +}(this, (function (exports, $) { 'use strict'; + + function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } + + var $__default = /*#__PURE__*/_interopDefaultLegacy($); + + /** + * -------------------------------------------- + * AdminLTE CardRefresh.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$e = 'CardRefresh'; + var DATA_KEY$e = 'lte.cardrefresh'; + var EVENT_KEY$7 = "." + DATA_KEY$e; + var JQUERY_NO_CONFLICT$e = $__default['default'].fn[NAME$e]; + var EVENT_LOADED = "loaded" + EVENT_KEY$7; + var EVENT_OVERLAY_ADDED = "overlay.added" + EVENT_KEY$7; + var EVENT_OVERLAY_REMOVED = "overlay.removed" + EVENT_KEY$7; + var CLASS_NAME_CARD$1 = 'card'; + var SELECTOR_CARD$1 = "." + CLASS_NAME_CARD$1; + var SELECTOR_DATA_REFRESH = '[data-card-widget="card-refresh"]'; + var Default$c = { + source: '', + sourceSelector: '', + params: {}, + trigger: SELECTOR_DATA_REFRESH, + content: '.card-body', + loadInContent: true, + loadOnInit: true, + responseType: '', + overlayTemplate: '
', + onLoadStart: function onLoadStart() {}, + onLoadDone: function onLoadDone(response) { + return response; + } + }; + + var CardRefresh = /*#__PURE__*/function () { + function CardRefresh(element, settings) { + this._element = element; + this._parent = element.parents(SELECTOR_CARD$1).first(); + this._settings = $__default['default'].extend({}, Default$c, settings); + this._overlay = $__default['default'](this._settings.overlayTemplate); + + if (element.hasClass(CLASS_NAME_CARD$1)) { + this._parent = element; + } + + if (this._settings.source === '') { + throw new Error('Source url was not defined. Please specify a url in your CardRefresh source option.'); + } + } + + var _proto = CardRefresh.prototype; + + _proto.load = function load() { + var _this = this; + + this._addOverlay(); + + this._settings.onLoadStart.call($__default['default'](this)); + + $__default['default'].get(this._settings.source, this._settings.params, function (response) { + if (_this._settings.loadInContent) { + if (_this._settings.sourceSelector !== '') { + response = $__default['default'](response).find(_this._settings.sourceSelector).html(); + } + + _this._parent.find(_this._settings.content).html(response); + } + + _this._settings.onLoadDone.call($__default['default'](_this), response); + + _this._removeOverlay(); + }, this._settings.responseType !== '' && this._settings.responseType); + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_LOADED)); + }; + + _proto._addOverlay = function _addOverlay() { + this._parent.append(this._overlay); + + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_OVERLAY_ADDED)); + }; + + _proto._removeOverlay = function _removeOverlay() { + this._parent.find(this._overlay).remove(); + + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_OVERLAY_REMOVED)); + } // Private + ; + + _proto._init = function _init() { + var _this2 = this; + + $__default['default'](this).find(this._settings.trigger).on('click', function () { + _this2.load(); + }); + + if (this._settings.loadOnInit) { + this.load(); + } + } // Static + ; + + CardRefresh._jQueryInterface = function _jQueryInterface(config) { + var data = $__default['default'](this).data(DATA_KEY$e); + + var _options = $__default['default'].extend({}, Default$c, $__default['default'](this).data()); + + if (!data) { + data = new CardRefresh($__default['default'](this), _options); + $__default['default'](this).data(DATA_KEY$e, typeof config === 'string' ? data : config); + } + + if (typeof config === 'string' && /load/.test(config)) { + data[config](); + } else { + data._init($__default['default'](this)); + } + }; + + return CardRefresh; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_DATA_REFRESH, function (event) { + if (event) { + event.preventDefault(); + } + + CardRefresh._jQueryInterface.call($__default['default'](this), 'load'); + }); + $__default['default'](function () { + $__default['default'](SELECTOR_DATA_REFRESH).each(function () { + CardRefresh._jQueryInterface.call($__default['default'](this)); + }); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$e] = CardRefresh._jQueryInterface; + $__default['default'].fn[NAME$e].Constructor = CardRefresh; + + $__default['default'].fn[NAME$e].noConflict = function () { + $__default['default'].fn[NAME$e] = JQUERY_NO_CONFLICT$e; + return CardRefresh._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE CardWidget.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$d = 'CardWidget'; + var DATA_KEY$d = 'lte.cardwidget'; + var EVENT_KEY$6 = "." + DATA_KEY$d; + var JQUERY_NO_CONFLICT$d = $__default['default'].fn[NAME$d]; + var EVENT_EXPANDED$3 = "expanded" + EVENT_KEY$6; + var EVENT_COLLAPSED$4 = "collapsed" + EVENT_KEY$6; + var EVENT_MAXIMIZED = "maximized" + EVENT_KEY$6; + var EVENT_MINIMIZED = "minimized" + EVENT_KEY$6; + var EVENT_REMOVED$1 = "removed" + EVENT_KEY$6; + var CLASS_NAME_CARD = 'card'; + var CLASS_NAME_COLLAPSED$1 = 'collapsed-card'; + var CLASS_NAME_COLLAPSING = 'collapsing-card'; + var CLASS_NAME_EXPANDING = 'expanding-card'; + var CLASS_NAME_WAS_COLLAPSED = 'was-collapsed'; + var CLASS_NAME_MAXIMIZED = 'maximized-card'; + var SELECTOR_DATA_REMOVE = '[data-card-widget="remove"]'; + var SELECTOR_DATA_COLLAPSE = '[data-card-widget="collapse"]'; + var SELECTOR_DATA_MAXIMIZE = '[data-card-widget="maximize"]'; + var SELECTOR_CARD = "." + CLASS_NAME_CARD; + var SELECTOR_CARD_HEADER = '.card-header'; + var SELECTOR_CARD_BODY = '.card-body'; + var SELECTOR_CARD_FOOTER = '.card-footer'; + var Default$b = { + animationSpeed: 'normal', + collapseTrigger: SELECTOR_DATA_COLLAPSE, + removeTrigger: SELECTOR_DATA_REMOVE, + maximizeTrigger: SELECTOR_DATA_MAXIMIZE, + collapseIcon: 'fa-minus', + expandIcon: 'fa-plus', + maximizeIcon: 'fa-expand', + minimizeIcon: 'fa-compress' + }; + + var CardWidget = /*#__PURE__*/function () { + function CardWidget(element, settings) { + this._element = element; + this._parent = element.parents(SELECTOR_CARD).first(); + + if (element.hasClass(CLASS_NAME_CARD)) { + this._parent = element; + } + + this._settings = $__default['default'].extend({}, Default$b, settings); + } + + var _proto = CardWidget.prototype; + + _proto.collapse = function collapse() { + var _this = this; + + this._parent.addClass(CLASS_NAME_COLLAPSING).children(SELECTOR_CARD_BODY + ", " + SELECTOR_CARD_FOOTER).slideUp(this._settings.animationSpeed, function () { + _this._parent.addClass(CLASS_NAME_COLLAPSED$1).removeClass(CLASS_NAME_COLLAPSING); + }); + + this._parent.find("> " + SELECTOR_CARD_HEADER + " " + this._settings.collapseTrigger + " ." + this._settings.collapseIcon).addClass(this._settings.expandIcon).removeClass(this._settings.collapseIcon); + + this._element.trigger($__default['default'].Event(EVENT_COLLAPSED$4), this._parent); + }; + + _proto.expand = function expand() { + var _this2 = this; + + this._parent.addClass(CLASS_NAME_EXPANDING).children(SELECTOR_CARD_BODY + ", " + SELECTOR_CARD_FOOTER).slideDown(this._settings.animationSpeed, function () { + _this2._parent.removeClass(CLASS_NAME_COLLAPSED$1).removeClass(CLASS_NAME_EXPANDING); + }); + + this._parent.find("> " + SELECTOR_CARD_HEADER + " " + this._settings.collapseTrigger + " ." + this._settings.expandIcon).addClass(this._settings.collapseIcon).removeClass(this._settings.expandIcon); + + this._element.trigger($__default['default'].Event(EVENT_EXPANDED$3), this._parent); + }; + + _proto.remove = function remove() { + this._parent.slideUp(); + + this._element.trigger($__default['default'].Event(EVENT_REMOVED$1), this._parent); + }; + + _proto.toggle = function toggle() { + if (this._parent.hasClass(CLASS_NAME_COLLAPSED$1)) { + this.expand(); + return; + } + + this.collapse(); + }; + + _proto.maximize = function maximize() { + this._parent.find(this._settings.maximizeTrigger + " ." + this._settings.maximizeIcon).addClass(this._settings.minimizeIcon).removeClass(this._settings.maximizeIcon); + + this._parent.css({ + height: this._parent.height(), + width: this._parent.width(), + transition: 'all .15s' + }).delay(150).queue(function () { + var $element = $__default['default'](this); + $element.addClass(CLASS_NAME_MAXIMIZED); + $__default['default']('html').addClass(CLASS_NAME_MAXIMIZED); + + if ($element.hasClass(CLASS_NAME_COLLAPSED$1)) { + $element.addClass(CLASS_NAME_WAS_COLLAPSED); + } + + $element.dequeue(); + }); + + this._element.trigger($__default['default'].Event(EVENT_MAXIMIZED), this._parent); + }; + + _proto.minimize = function minimize() { + this._parent.find(this._settings.maximizeTrigger + " ." + this._settings.minimizeIcon).addClass(this._settings.maximizeIcon).removeClass(this._settings.minimizeIcon); + + this._parent.css('cssText', "height: " + this._parent[0].style.height + " !important; width: " + this._parent[0].style.width + " !important; transition: all .15s;").delay(10).queue(function () { + var $element = $__default['default'](this); + $element.removeClass(CLASS_NAME_MAXIMIZED); + $__default['default']('html').removeClass(CLASS_NAME_MAXIMIZED); + $element.css({ + height: 'inherit', + width: 'inherit' + }); + + if ($element.hasClass(CLASS_NAME_WAS_COLLAPSED)) { + $element.removeClass(CLASS_NAME_WAS_COLLAPSED); + } + + $element.dequeue(); + }); + + this._element.trigger($__default['default'].Event(EVENT_MINIMIZED), this._parent); + }; + + _proto.toggleMaximize = function toggleMaximize() { + if (this._parent.hasClass(CLASS_NAME_MAXIMIZED)) { + this.minimize(); + return; + } + + this.maximize(); + } // Private + ; + + _proto._init = function _init(card) { + var _this3 = this; + + this._parent = card; + $__default['default'](this).find(this._settings.collapseTrigger).click(function () { + _this3.toggle(); + }); + $__default['default'](this).find(this._settings.maximizeTrigger).click(function () { + _this3.toggleMaximize(); + }); + $__default['default'](this).find(this._settings.removeTrigger).click(function () { + _this3.remove(); + }); + } // Static + ; + + CardWidget._jQueryInterface = function _jQueryInterface(config) { + var data = $__default['default'](this).data(DATA_KEY$d); + + var _options = $__default['default'].extend({}, Default$b, $__default['default'](this).data()); + + if (!data) { + data = new CardWidget($__default['default'](this), _options); + $__default['default'](this).data(DATA_KEY$d, typeof config === 'string' ? data : config); + } + + if (typeof config === 'string' && /collapse|expand|remove|toggle|maximize|minimize|toggleMaximize/.test(config)) { + data[config](); + } else if (typeof config === 'object') { + data._init($__default['default'](this)); + } + }; + + return CardWidget; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_DATA_COLLAPSE, function (event) { + if (event) { + event.preventDefault(); + } + + CardWidget._jQueryInterface.call($__default['default'](this), 'toggle'); + }); + $__default['default'](document).on('click', SELECTOR_DATA_REMOVE, function (event) { + if (event) { + event.preventDefault(); + } + + CardWidget._jQueryInterface.call($__default['default'](this), 'remove'); + }); + $__default['default'](document).on('click', SELECTOR_DATA_MAXIMIZE, function (event) { + if (event) { + event.preventDefault(); + } + + CardWidget._jQueryInterface.call($__default['default'](this), 'toggleMaximize'); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$d] = CardWidget._jQueryInterface; + $__default['default'].fn[NAME$d].Constructor = CardWidget; + + $__default['default'].fn[NAME$d].noConflict = function () { + $__default['default'].fn[NAME$d] = JQUERY_NO_CONFLICT$d; + return CardWidget._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE ControlSidebar.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$c = 'ControlSidebar'; + var DATA_KEY$c = 'lte.controlsidebar'; + var EVENT_KEY$5 = "." + DATA_KEY$c; + var JQUERY_NO_CONFLICT$c = $__default['default'].fn[NAME$c]; + var EVENT_COLLAPSED$3 = "collapsed" + EVENT_KEY$5; + var EVENT_EXPANDED$2 = "expanded" + EVENT_KEY$5; + var SELECTOR_CONTROL_SIDEBAR = '.control-sidebar'; + var SELECTOR_CONTROL_SIDEBAR_CONTENT$1 = '.control-sidebar-content'; + var SELECTOR_DATA_TOGGLE$4 = '[data-widget="control-sidebar"]'; + var SELECTOR_HEADER$1 = '.main-header'; + var SELECTOR_FOOTER$1 = '.main-footer'; + var CLASS_NAME_CONTROL_SIDEBAR_ANIMATE = 'control-sidebar-animate'; + var CLASS_NAME_CONTROL_SIDEBAR_OPEN$1 = 'control-sidebar-open'; + var CLASS_NAME_CONTROL_SIDEBAR_SLIDE = 'control-sidebar-slide-open'; + var CLASS_NAME_LAYOUT_FIXED$1 = 'layout-fixed'; + var CLASS_NAME_NAVBAR_FIXED = 'layout-navbar-fixed'; + var CLASS_NAME_NAVBAR_SM_FIXED = 'layout-sm-navbar-fixed'; + var CLASS_NAME_NAVBAR_MD_FIXED = 'layout-md-navbar-fixed'; + var CLASS_NAME_NAVBAR_LG_FIXED = 'layout-lg-navbar-fixed'; + var CLASS_NAME_NAVBAR_XL_FIXED = 'layout-xl-navbar-fixed'; + var CLASS_NAME_FOOTER_FIXED = 'layout-footer-fixed'; + var CLASS_NAME_FOOTER_SM_FIXED = 'layout-sm-footer-fixed'; + var CLASS_NAME_FOOTER_MD_FIXED = 'layout-md-footer-fixed'; + var CLASS_NAME_FOOTER_LG_FIXED = 'layout-lg-footer-fixed'; + var CLASS_NAME_FOOTER_XL_FIXED = 'layout-xl-footer-fixed'; + var Default$a = { + controlsidebarSlide: true, + scrollbarTheme: 'os-theme-light', + scrollbarAutoHide: 'l', + target: SELECTOR_CONTROL_SIDEBAR + }; + /** + * Class Definition + * ==================================================== + */ + + var ControlSidebar = /*#__PURE__*/function () { + function ControlSidebar(element, config) { + this._element = element; + this._config = config; + } // Public + + + var _proto = ControlSidebar.prototype; + + _proto.collapse = function collapse() { + var $body = $__default['default']('body'); + var $html = $__default['default']('html'); + var target = this._config.target; // Show the control sidebar + + if (this._config.controlsidebarSlide) { + $html.addClass(CLASS_NAME_CONTROL_SIDEBAR_ANIMATE); + $body.removeClass(CLASS_NAME_CONTROL_SIDEBAR_SLIDE).delay(300).queue(function () { + $__default['default'](target).hide(); + $html.removeClass(CLASS_NAME_CONTROL_SIDEBAR_ANIMATE); + $__default['default'](this).dequeue(); + }); + } else { + $body.removeClass(CLASS_NAME_CONTROL_SIDEBAR_OPEN$1); + } + + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_COLLAPSED$3)); + }; + + _proto.show = function show() { + var $body = $__default['default']('body'); + var $html = $__default['default']('html'); // Collapse the control sidebar + + if (this._config.controlsidebarSlide) { + $html.addClass(CLASS_NAME_CONTROL_SIDEBAR_ANIMATE); + $__default['default'](this._config.target).show().delay(10).queue(function () { + $body.addClass(CLASS_NAME_CONTROL_SIDEBAR_SLIDE).delay(300).queue(function () { + $html.removeClass(CLASS_NAME_CONTROL_SIDEBAR_ANIMATE); + $__default['default'](this).dequeue(); + }); + $__default['default'](this).dequeue(); + }); + } else { + $body.addClass(CLASS_NAME_CONTROL_SIDEBAR_OPEN$1); + } + + this._fixHeight(); + + this._fixScrollHeight(); + + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_EXPANDED$2)); + }; + + _proto.toggle = function toggle() { + var $body = $__default['default']('body'); + var shouldClose = $body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_OPEN$1) || $body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_SLIDE); + + if (shouldClose) { + // Close the control sidebar + this.collapse(); + } else { + // Open the control sidebar + this.show(); + } + } // Private + ; + + _proto._init = function _init() { + var _this = this; + + var $body = $__default['default']('body'); + var shouldNotHideAll = $body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_OPEN$1) || $body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_SLIDE); + + if (shouldNotHideAll) { + $__default['default'](SELECTOR_CONTROL_SIDEBAR).not(this._config.target).hide(); + $__default['default'](this._config.target).css('display', 'block'); + } else { + $__default['default'](SELECTOR_CONTROL_SIDEBAR).hide(); + } + + this._fixHeight(); + + this._fixScrollHeight(); + + $__default['default'](window).resize(function () { + _this._fixHeight(); + + _this._fixScrollHeight(); + }); + $__default['default'](window).scroll(function () { + var $body = $__default['default']('body'); + var shouldFixHeight = $body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_OPEN$1) || $body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_SLIDE); + + if (shouldFixHeight) { + _this._fixScrollHeight(); + } + }); + }; + + _proto._isNavbarFixed = function _isNavbarFixed() { + var $body = $__default['default']('body'); + return $body.hasClass(CLASS_NAME_NAVBAR_FIXED) || $body.hasClass(CLASS_NAME_NAVBAR_SM_FIXED) || $body.hasClass(CLASS_NAME_NAVBAR_MD_FIXED) || $body.hasClass(CLASS_NAME_NAVBAR_LG_FIXED) || $body.hasClass(CLASS_NAME_NAVBAR_XL_FIXED); + }; + + _proto._isFooterFixed = function _isFooterFixed() { + var $body = $__default['default']('body'); + return $body.hasClass(CLASS_NAME_FOOTER_FIXED) || $body.hasClass(CLASS_NAME_FOOTER_SM_FIXED) || $body.hasClass(CLASS_NAME_FOOTER_MD_FIXED) || $body.hasClass(CLASS_NAME_FOOTER_LG_FIXED) || $body.hasClass(CLASS_NAME_FOOTER_XL_FIXED); + }; + + _proto._fixScrollHeight = function _fixScrollHeight() { + var $body = $__default['default']('body'); + var $controlSidebar = $__default['default'](this._config.target); + + if (!$body.hasClass(CLASS_NAME_LAYOUT_FIXED$1)) { + return; + } + + var heights = { + scroll: $__default['default'](document).height(), + window: $__default['default'](window).height(), + header: $__default['default'](SELECTOR_HEADER$1).outerHeight(), + footer: $__default['default'](SELECTOR_FOOTER$1).outerHeight() + }; + var positions = { + bottom: Math.abs(heights.window + $__default['default'](window).scrollTop() - heights.scroll), + top: $__default['default'](window).scrollTop() + }; + var navbarFixed = this._isNavbarFixed() && $__default['default'](SELECTOR_HEADER$1).css('position') === 'fixed'; + var footerFixed = this._isFooterFixed() && $__default['default'](SELECTOR_FOOTER$1).css('position') === 'fixed'; + var $controlsidebarContent = $__default['default'](this._config.target + ", " + this._config.target + " " + SELECTOR_CONTROL_SIDEBAR_CONTENT$1); + + if (positions.top === 0 && positions.bottom === 0) { + $controlSidebar.css({ + bottom: heights.footer, + top: heights.header + }); + $controlsidebarContent.css('height', heights.window - (heights.header + heights.footer)); + } else if (positions.bottom <= heights.footer) { + if (footerFixed === false) { + var top = heights.header - positions.top; + $controlSidebar.css('bottom', heights.footer - positions.bottom).css('top', top >= 0 ? top : 0); + $controlsidebarContent.css('height', heights.window - (heights.footer - positions.bottom)); + } else { + $controlSidebar.css('bottom', heights.footer); + } + } else if (positions.top <= heights.header) { + if (navbarFixed === false) { + $controlSidebar.css('top', heights.header - positions.top); + $controlsidebarContent.css('height', heights.window - (heights.header - positions.top)); + } else { + $controlSidebar.css('top', heights.header); + } + } else if (navbarFixed === false) { + $controlSidebar.css('top', 0); + $controlsidebarContent.css('height', heights.window); + } else { + $controlSidebar.css('top', heights.header); + } + + if (footerFixed && navbarFixed) { + $controlsidebarContent.css('height', '100%'); + $controlSidebar.css('height', ''); + } else if (footerFixed || navbarFixed) { + $controlsidebarContent.css('height', '100%'); + $controlsidebarContent.css('height', ''); + } + }; + + _proto._fixHeight = function _fixHeight() { + var $body = $__default['default']('body'); + var $controlSidebar = $__default['default'](this._config.target + " " + SELECTOR_CONTROL_SIDEBAR_CONTENT$1); + + if (!$body.hasClass(CLASS_NAME_LAYOUT_FIXED$1)) { + $controlSidebar.attr('style', ''); + return; + } + + var heights = { + window: $__default['default'](window).height(), + header: $__default['default'](SELECTOR_HEADER$1).outerHeight(), + footer: $__default['default'](SELECTOR_FOOTER$1).outerHeight() + }; + var sidebarHeight = heights.window - heights.header; + + if (this._isFooterFixed() && $__default['default'](SELECTOR_FOOTER$1).css('position') === 'fixed') { + sidebarHeight = heights.window - heights.header - heights.footer; + } + + $controlSidebar.css('height', sidebarHeight); + + if (typeof $__default['default'].fn.overlayScrollbars !== 'undefined') { + $controlSidebar.overlayScrollbars({ + className: this._config.scrollbarTheme, + sizeAutoCapable: true, + scrollbars: { + autoHide: this._config.scrollbarAutoHide, + clickScrolling: true + } + }); + } + } // Static + ; + + ControlSidebar._jQueryInterface = function _jQueryInterface(operation) { + return this.each(function () { + var data = $__default['default'](this).data(DATA_KEY$c); + + var _options = $__default['default'].extend({}, Default$a, $__default['default'](this).data()); + + if (!data) { + data = new ControlSidebar(this, _options); + $__default['default'](this).data(DATA_KEY$c, data); + } + + if (data[operation] === 'undefined') { + throw new Error(operation + " is not a function"); + } + + data[operation](); + }); + }; + + return ControlSidebar; + }(); + /** + * + * Data Api implementation + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_DATA_TOGGLE$4, function (event) { + event.preventDefault(); + + ControlSidebar._jQueryInterface.call($__default['default'](this), 'toggle'); + }); + $__default['default'](document).ready(function () { + ControlSidebar._jQueryInterface.call($__default['default'](SELECTOR_DATA_TOGGLE$4), '_init'); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$c] = ControlSidebar._jQueryInterface; + $__default['default'].fn[NAME$c].Constructor = ControlSidebar; + + $__default['default'].fn[NAME$c].noConflict = function () { + $__default['default'].fn[NAME$c] = JQUERY_NO_CONFLICT$c; + return ControlSidebar._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE DirectChat.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$b = 'DirectChat'; + var DATA_KEY$b = 'lte.directchat'; + var EVENT_KEY$4 = "." + DATA_KEY$b; + var JQUERY_NO_CONFLICT$b = $__default['default'].fn[NAME$b]; + var EVENT_TOGGLED = "toggled" + EVENT_KEY$4; + var SELECTOR_DATA_TOGGLE$3 = '[data-widget="chat-pane-toggle"]'; + var SELECTOR_DIRECT_CHAT = '.direct-chat'; + var CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open'; + /** + * Class Definition + * ==================================================== + */ + + var DirectChat = /*#__PURE__*/function () { + function DirectChat(element) { + this._element = element; + } + + var _proto = DirectChat.prototype; + + _proto.toggle = function toggle() { + $__default['default'](this._element).parents(SELECTOR_DIRECT_CHAT).first().toggleClass(CLASS_NAME_DIRECT_CHAT_OPEN); + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_TOGGLED)); + } // Static + ; + + DirectChat._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $__default['default'](this).data(DATA_KEY$b); + + if (!data) { + data = new DirectChat($__default['default'](this)); + $__default['default'](this).data(DATA_KEY$b, data); + } + + data[config](); + }); + }; + + return DirectChat; + }(); + /** + * + * Data Api implementation + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_DATA_TOGGLE$3, function (event) { + if (event) { + event.preventDefault(); + } + + DirectChat._jQueryInterface.call($__default['default'](this), 'toggle'); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$b] = DirectChat._jQueryInterface; + $__default['default'].fn[NAME$b].Constructor = DirectChat; + + $__default['default'].fn[NAME$b].noConflict = function () { + $__default['default'].fn[NAME$b] = JQUERY_NO_CONFLICT$b; + return DirectChat._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE Dropdown.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$a = 'Dropdown'; + var DATA_KEY$a = 'lte.dropdown'; + var JQUERY_NO_CONFLICT$a = $__default['default'].fn[NAME$a]; + var SELECTOR_NAVBAR = '.navbar'; + var SELECTOR_DROPDOWN_MENU = '.dropdown-menu'; + var SELECTOR_DROPDOWN_MENU_ACTIVE = '.dropdown-menu.show'; + var SELECTOR_DROPDOWN_TOGGLE = '[data-toggle="dropdown"]'; + var CLASS_NAME_DROPDOWN_RIGHT = 'dropdown-menu-right'; + var CLASS_NAME_DROPDOWN_SUBMENU = 'dropdown-submenu'; // TODO: this is unused; should be removed along with the extend? + + var Default$9 = {}; + /** + * Class Definition + * ==================================================== + */ + + var Dropdown = /*#__PURE__*/function () { + function Dropdown(element, config) { + this._config = config; + this._element = element; + } // Public + + + var _proto = Dropdown.prototype; + + _proto.toggleSubmenu = function toggleSubmenu() { + this._element.siblings().show().toggleClass('show'); + + if (!this._element.next().hasClass('show')) { + this._element.parents(SELECTOR_DROPDOWN_MENU).first().find('.show').removeClass('show').hide(); + } + + this._element.parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function () { + $__default['default']('.dropdown-submenu .show').removeClass('show').hide(); + }); + }; + + _proto.fixPosition = function fixPosition() { + var $element = $__default['default'](SELECTOR_DROPDOWN_MENU_ACTIVE); + + if ($element.length === 0) { + return; + } + + if ($element.hasClass(CLASS_NAME_DROPDOWN_RIGHT)) { + $element.css({ + left: 'inherit', + right: 0 + }); + } else { + $element.css({ + left: 0, + right: 'inherit' + }); + } + + var offset = $element.offset(); + var width = $element.width(); + var visiblePart = $__default['default'](window).width() - offset.left; + + if (offset.left < 0) { + $element.css({ + left: 'inherit', + right: offset.left - 5 + }); + } else if (visiblePart < width) { + $element.css({ + left: 'inherit', + right: 0 + }); + } + } // Static + ; + + Dropdown._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $__default['default'](this).data(DATA_KEY$a); + + var _config = $__default['default'].extend({}, Default$9, $__default['default'](this).data()); + + if (!data) { + data = new Dropdown($__default['default'](this), _config); + $__default['default'](this).data(DATA_KEY$a, data); + } + + if (config === 'toggleSubmenu' || config === 'fixPosition') { + data[config](); + } + }); + }; + + return Dropdown; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](SELECTOR_DROPDOWN_MENU + " " + SELECTOR_DROPDOWN_TOGGLE).on('click', function (event) { + event.preventDefault(); + event.stopPropagation(); + + Dropdown._jQueryInterface.call($__default['default'](this), 'toggleSubmenu'); + }); + $__default['default'](SELECTOR_NAVBAR + " " + SELECTOR_DROPDOWN_TOGGLE).on('click', function (event) { + event.preventDefault(); + + if ($__default['default'](event.target).parent().hasClass(CLASS_NAME_DROPDOWN_SUBMENU)) { + return; + } + + setTimeout(function () { + Dropdown._jQueryInterface.call($__default['default'](this), 'fixPosition'); + }, 1); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$a] = Dropdown._jQueryInterface; + $__default['default'].fn[NAME$a].Constructor = Dropdown; + + $__default['default'].fn[NAME$a].noConflict = function () { + $__default['default'].fn[NAME$a] = JQUERY_NO_CONFLICT$a; + return Dropdown._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE ExpandableTable.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$9 = 'ExpandableTable'; + var DATA_KEY$9 = 'lte.expandableTable'; + var EVENT_KEY$3 = "." + DATA_KEY$9; + var JQUERY_NO_CONFLICT$9 = $__default['default'].fn[NAME$9]; + var EVENT_EXPANDED$1 = "expanded" + EVENT_KEY$3; + var EVENT_COLLAPSED$2 = "collapsed" + EVENT_KEY$3; + var SELECTOR_TABLE = '.expandable-table'; + var SELECTOR_EXPANDABLE_BODY = '.expandable-body'; + var SELECTOR_DATA_TOGGLE$2 = '[data-widget="expandable-table"]'; + var SELECTOR_ARIA_ATTR = 'aria-expanded'; + /** + * Class Definition + * ==================================================== + */ + + var ExpandableTable = /*#__PURE__*/function () { + function ExpandableTable(element, options) { + this._options = options; + this._element = element; + } // Public + + + var _proto = ExpandableTable.prototype; + + _proto.init = function init() { + $__default['default'](SELECTOR_DATA_TOGGLE$2).each(function (_, $header) { + var $type = $__default['default']($header).attr(SELECTOR_ARIA_ATTR); + var $body = $__default['default']($header).next(SELECTOR_EXPANDABLE_BODY).children().first().children(); + + if ($type === 'true') { + $body.show(); + } else if ($type === 'false') { + $body.hide(); + $body.parent().parent().addClass('d-none'); + } + }); + }; + + _proto.toggleRow = function toggleRow() { + var $element = this._element; + var time = 500; + var $type = $element.attr(SELECTOR_ARIA_ATTR); + var $body = $element.next(SELECTOR_EXPANDABLE_BODY).children().first().children(); + $body.stop(); + + if ($type === 'true') { + $body.slideUp(time, function () { + $element.next(SELECTOR_EXPANDABLE_BODY).addClass('d-none'); + }); + $element.attr(SELECTOR_ARIA_ATTR, 'false'); + $element.trigger($__default['default'].Event(EVENT_COLLAPSED$2)); + } else if ($type === 'false') { + $element.next(SELECTOR_EXPANDABLE_BODY).removeClass('d-none'); + $body.slideDown(time); + $element.attr(SELECTOR_ARIA_ATTR, 'true'); + $element.trigger($__default['default'].Event(EVENT_EXPANDED$1)); + } + } // Static + ; + + ExpandableTable._jQueryInterface = function _jQueryInterface(operation) { + return this.each(function () { + var data = $__default['default'](this).data(DATA_KEY$9); + + if (!data) { + data = new ExpandableTable($__default['default'](this)); + $__default['default'](this).data(DATA_KEY$9, data); + } + + if (typeof operation === 'string' && /init|toggleRow/.test(operation)) { + data[operation](); + } + }); + }; + + return ExpandableTable; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](SELECTOR_TABLE).ready(function () { + ExpandableTable._jQueryInterface.call($__default['default'](this), 'init'); + }); + $__default['default'](document).on('click', SELECTOR_DATA_TOGGLE$2, function () { + ExpandableTable._jQueryInterface.call($__default['default'](this), 'toggleRow'); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$9] = ExpandableTable._jQueryInterface; + $__default['default'].fn[NAME$9].Constructor = ExpandableTable; + + $__default['default'].fn[NAME$9].noConflict = function () { + $__default['default'].fn[NAME$9] = JQUERY_NO_CONFLICT$9; + return ExpandableTable._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE Fullscreen.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$8 = 'Fullscreen'; + var DATA_KEY$8 = 'lte.fullscreen'; + var JQUERY_NO_CONFLICT$8 = $__default['default'].fn[NAME$8]; + var SELECTOR_DATA_WIDGET$2 = '[data-widget="fullscreen"]'; + var SELECTOR_ICON = SELECTOR_DATA_WIDGET$2 + " i"; + var Default$8 = { + minimizeIcon: 'fa-compress-arrows-alt', + maximizeIcon: 'fa-expand-arrows-alt' + }; + /** + * Class Definition + * ==================================================== + */ + + var Fullscreen = /*#__PURE__*/function () { + function Fullscreen(_element, _options) { + this.element = _element; + this.options = $__default['default'].extend({}, Default$8, _options); + } // Public + + + var _proto = Fullscreen.prototype; + + _proto.toggle = function toggle() { + if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) { + this.windowed(); + } else { + this.fullscreen(); + } + }; + + _proto.fullscreen = function fullscreen() { + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(); + } else if (document.documentElement.msRequestFullscreen) { + document.documentElement.msRequestFullscreen(); + } + + $__default['default'](SELECTOR_ICON).removeClass(this.options.maximizeIcon).addClass(this.options.minimizeIcon); + }; + + _proto.windowed = function windowed() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + + $__default['default'](SELECTOR_ICON).removeClass(this.options.minimizeIcon).addClass(this.options.maximizeIcon); + } // Static + ; + + Fullscreen._jQueryInterface = function _jQueryInterface(config) { + var data = $__default['default'](this).data(DATA_KEY$8); + + if (!data) { + data = $__default['default'](this).data(); + } + + var _options = $__default['default'].extend({}, Default$8, typeof config === 'object' ? config : data); + + var plugin = new Fullscreen($__default['default'](this), _options); + $__default['default'](this).data(DATA_KEY$8, typeof config === 'object' ? config : data); + + if (typeof config === 'string' && /toggle|fullscreen|windowed/.test(config)) { + plugin[config](); + } else { + plugin.init(); + } + }; + + return Fullscreen; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_DATA_WIDGET$2, function () { + Fullscreen._jQueryInterface.call($__default['default'](this), 'toggle'); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$8] = Fullscreen._jQueryInterface; + $__default['default'].fn[NAME$8].Constructor = Fullscreen; + + $__default['default'].fn[NAME$8].noConflict = function () { + $__default['default'].fn[NAME$8] = JQUERY_NO_CONFLICT$8; + return Fullscreen._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE IFrame.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$7 = 'IFrame'; + var DATA_KEY$7 = 'lte.iframe'; + var JQUERY_NO_CONFLICT$7 = $__default['default'].fn[NAME$7]; + var SELECTOR_DATA_TOGGLE$1 = '[data-widget="iframe"]'; + var SELECTOR_DATA_TOGGLE_CLOSE = '[data-widget="iframe-close"]'; + var SELECTOR_DATA_TOGGLE_SCROLL_LEFT = '[data-widget="iframe-scrollleft"]'; + var SELECTOR_DATA_TOGGLE_SCROLL_RIGHT = '[data-widget="iframe-scrollright"]'; + var SELECTOR_DATA_TOGGLE_FULLSCREEN = '[data-widget="iframe-fullscreen"]'; + var SELECTOR_CONTENT_WRAPPER = '.content-wrapper'; + var SELECTOR_CONTENT_IFRAME = SELECTOR_CONTENT_WRAPPER + " iframe"; + var SELECTOR_TAB_NAV = SELECTOR_DATA_TOGGLE$1 + ".iframe-mode .nav"; + var SELECTOR_TAB_NAVBAR_NAV = SELECTOR_DATA_TOGGLE$1 + ".iframe-mode .navbar-nav"; + var SELECTOR_TAB_NAVBAR_NAV_ITEM = SELECTOR_TAB_NAVBAR_NAV + " .nav-item"; + var SELECTOR_TAB_NAVBAR_NAV_LINK = SELECTOR_TAB_NAVBAR_NAV + " .nav-link"; + var SELECTOR_TAB_CONTENT = SELECTOR_DATA_TOGGLE$1 + ".iframe-mode .tab-content"; + var SELECTOR_TAB_EMPTY = SELECTOR_TAB_CONTENT + " .tab-empty"; + var SELECTOR_TAB_LOADING = SELECTOR_TAB_CONTENT + " .tab-loading"; + var SELECTOR_TAB_PANE = SELECTOR_TAB_CONTENT + " .tab-pane"; + var SELECTOR_SIDEBAR_MENU_ITEM = '.main-sidebar .nav-item > a.nav-link'; + var SELECTOR_SIDEBAR_SEARCH_ITEM = '.sidebar-search-results .list-group-item'; + var SELECTOR_HEADER_MENU_ITEM = '.main-header .nav-item a.nav-link'; + var SELECTOR_HEADER_DROPDOWN_ITEM = '.main-header a.dropdown-item'; + var CLASS_NAME_IFRAME_MODE = 'iframe-mode'; + var CLASS_NAME_FULLSCREEN_MODE = 'iframe-mode-fullscreen'; + var Default$7 = { + onTabClick: function onTabClick(item) { + return item; + }, + onTabChanged: function onTabChanged(item) { + return item; + }, + onTabCreated: function onTabCreated(item) { + return item; + }, + autoIframeMode: true, + autoItemActive: true, + autoShowNewTab: true, + allowDuplicates: false, + loadingScreen: true, + useNavbarItems: true, + scrollOffset: 40, + scrollBehaviorSwap: false, + iconMaximize: 'fa-expand', + iconMinimize: 'fa-compress' + }; + /** + * Class Definition + * ==================================================== + */ + + var IFrame = /*#__PURE__*/function () { + function IFrame(element, config) { + this._config = config; + this._element = element; + + this._init(); + } // Public + + + var _proto = IFrame.prototype; + + _proto.onTabClick = function onTabClick(item) { + this._config.onTabClick(item); + }; + + _proto.onTabChanged = function onTabChanged(item) { + this._config.onTabChanged(item); + }; + + _proto.onTabCreated = function onTabCreated(item) { + this._config.onTabCreated(item); + }; + + _proto.createTab = function createTab(title, link, uniqueName, autoOpen) { + var _this = this; + + var tabId = "panel-" + uniqueName; + var navId = "tab-" + uniqueName; + + if (this._config.allowDuplicates) { + tabId += "-" + Math.floor(Math.random() * 1000); + navId += "-" + Math.floor(Math.random() * 1000); + } + + var newNavItem = "
  • " + title + "
  • "; + $__default['default'](SELECTOR_TAB_NAVBAR_NAV).append(unescape(escape(newNavItem))); + var newTabItem = "
    "; + $__default['default'](SELECTOR_TAB_CONTENT).append(unescape(escape(newTabItem))); + + if (autoOpen) { + if (this._config.loadingScreen) { + var $loadingScreen = $__default['default'](SELECTOR_TAB_LOADING); + $loadingScreen.fadeIn(); + $__default['default'](tabId + " iframe").ready(function () { + if (typeof _this._config.loadingScreen === 'number') { + _this.switchTab("#" + navId); + + setTimeout(function () { + $loadingScreen.fadeOut(); + }, _this._config.loadingScreen); + } else { + _this.switchTab("#" + navId); + + $loadingScreen.fadeOut(); + } + }); + } else { + this.switchTab("#" + navId); + } + } + + this.onTabCreated($__default['default']("#" + navId)); + }; + + _proto.openTabSidebar = function openTabSidebar(item, autoOpen) { + if (autoOpen === void 0) { + autoOpen = this._config.autoShowNewTab; + } + + var $item = $__default['default'](item).clone(); + + if ($item.attr('href') === undefined) { + $item = $__default['default'](item).parent('a').clone(); + } + + $item.find('.right, .search-path').remove(); + var title = $item.find('p').text(); + + if (title === '') { + title = $item.text(); + } + + var link = $item.attr('href'); + + if (link === '#' || link === '' || link === undefined) { + return; + } + + var uniqueName = link.replace('./', '').replace(/["&'./:=?[\]]/gi, '-').replace(/(--)/gi, ''); + var navId = "tab-" + uniqueName; + + if (!this._config.allowDuplicates && $__default['default']("#" + navId).length > 0) { + return this.switchTab("#" + navId); + } + + if (!this._config.allowDuplicates && $__default['default']("#" + navId).length === 0 || this._config.allowDuplicates) { + this.createTab(title, link, uniqueName, autoOpen); + } + }; + + _proto.switchTab = function switchTab(item) { + var $item = $__default['default'](item); + var tabId = $item.attr('href'); + $__default['default'](SELECTOR_TAB_EMPTY).hide(); + $__default['default'](SELECTOR_TAB_NAVBAR_NAV + " .active").tab('dispose').removeClass('active'); + + this._fixHeight(); + + $item.tab('show'); + $item.parents('li').addClass('active'); + this.onTabChanged($item); + + if (this._config.autoItemActive) { + this._setItemActive($__default['default'](tabId + " iframe").attr('src')); + } + }; + + _proto.removeActiveTab = function removeActiveTab(type, element) { + if (type == 'all') { + $__default['default'](SELECTOR_TAB_NAVBAR_NAV_ITEM).remove(); + $__default['default'](SELECTOR_TAB_PANE).remove(); + $__default['default'](SELECTOR_TAB_EMPTY).show(); + } else if (type == 'all-other') { + $__default['default'](SELECTOR_TAB_NAVBAR_NAV_ITEM + ":not(.active)").remove(); + $__default['default'](SELECTOR_TAB_PANE + ":not(.active)").remove(); + } else if (type == 'only-this') { + var $navClose = $__default['default'](element); + var $navItem = $navClose.parent('.nav-item'); + var $navItemParent = $navItem.parent(); + var navItemIndex = $navItem.index(); + var tabId = $navClose.siblings('.nav-link').attr('aria-controls'); + $navItem.remove(); + $__default['default']("#" + tabId).remove(); + + if ($__default['default'](SELECTOR_TAB_CONTENT).children().length == $__default['default'](SELECTOR_TAB_EMPTY + ", " + SELECTOR_TAB_LOADING).length) { + $__default['default'](SELECTOR_TAB_EMPTY).show(); + } else { + var prevNavItemIndex = navItemIndex - 1; + this.switchTab($navItemParent.children().eq(prevNavItemIndex).find('a.nav-link')); + } + } else { + var _$navItem = $__default['default'](SELECTOR_TAB_NAVBAR_NAV_ITEM + ".active"); + + var _$navItemParent = _$navItem.parent(); + + var _navItemIndex = _$navItem.index(); + + _$navItem.remove(); + + $__default['default'](SELECTOR_TAB_PANE + ".active").remove(); + + if ($__default['default'](SELECTOR_TAB_CONTENT).children().length == $__default['default'](SELECTOR_TAB_EMPTY + ", " + SELECTOR_TAB_LOADING).length) { + $__default['default'](SELECTOR_TAB_EMPTY).show(); + } else { + var _prevNavItemIndex = _navItemIndex - 1; + + this.switchTab(_$navItemParent.children().eq(_prevNavItemIndex).find('a.nav-link')); + } + } + }; + + _proto.toggleFullscreen = function toggleFullscreen() { + if ($__default['default']('body').hasClass(CLASS_NAME_FULLSCREEN_MODE)) { + $__default['default'](SELECTOR_DATA_TOGGLE_FULLSCREEN + " i").removeClass(this._config.iconMinimize).addClass(this._config.iconMaximize); + $__default['default']('body').removeClass(CLASS_NAME_FULLSCREEN_MODE); + $__default['default'](SELECTOR_TAB_EMPTY + ", " + SELECTOR_TAB_LOADING).height('auto'); + $__default['default'](SELECTOR_CONTENT_WRAPPER).height('auto'); + $__default['default'](SELECTOR_CONTENT_IFRAME).height('auto'); + } else { + $__default['default'](SELECTOR_DATA_TOGGLE_FULLSCREEN + " i").removeClass(this._config.iconMaximize).addClass(this._config.iconMinimize); + $__default['default']('body').addClass(CLASS_NAME_FULLSCREEN_MODE); + } + + $__default['default'](window).trigger('resize'); + + this._fixHeight(true); + } // Private + ; + + _proto._init = function _init() { + if (window.frameElement && this._config.autoIframeMode) { + $__default['default']('body').addClass(CLASS_NAME_IFRAME_MODE); + } else if ($__default['default'](SELECTOR_CONTENT_WRAPPER).hasClass(CLASS_NAME_IFRAME_MODE)) { + if ($__default['default'](SELECTOR_TAB_CONTENT).children().length > 2) { + var $el = $__default['default'](SELECTOR_TAB_PANE + ":first-child"); + $el.show(); + + this._setItemActive($el.find('iframe').attr('src')); + } + + this._setupListeners(); + + this._fixHeight(true); + } + }; + + _proto._navScroll = function _navScroll(offset) { + var leftPos = $__default['default'](SELECTOR_TAB_NAVBAR_NAV).scrollLeft(); + $__default['default'](SELECTOR_TAB_NAVBAR_NAV).animate({ + scrollLeft: leftPos + offset + }, 250, 'linear'); + }; + + _proto._setupListeners = function _setupListeners() { + var _this2 = this; + + $__default['default'](window).on('resize', function () { + setTimeout(function () { + _this2._fixHeight(); + }, 1); + }); + $__default['default'](document).on('click', SELECTOR_SIDEBAR_MENU_ITEM + ", " + SELECTOR_SIDEBAR_SEARCH_ITEM, function (e) { + e.preventDefault(); + + _this2.openTabSidebar(e.target); + }); + + if (this._config.useNavbarItems) { + $__default['default'](document).on('click', SELECTOR_HEADER_MENU_ITEM + ", " + SELECTOR_HEADER_DROPDOWN_ITEM, function (e) { + e.preventDefault(); + + _this2.openTabSidebar(e.target); + }); + } + + $__default['default'](document).on('click', SELECTOR_TAB_NAVBAR_NAV_LINK, function (e) { + e.preventDefault(); + + _this2.onTabClick(e.target); + + _this2.switchTab(e.target); + }); + $__default['default'](document).on('click', SELECTOR_TAB_NAVBAR_NAV_LINK, function (e) { + e.preventDefault(); + + _this2.onTabClick(e.target); + + _this2.switchTab(e.target); + }); + $__default['default'](document).on('click', SELECTOR_DATA_TOGGLE_CLOSE, function (e) { + e.preventDefault(); + var target = e.target; + + if (target.nodeName == 'I') { + target = e.target.offsetParent; + } + + _this2.removeActiveTab(target.attributes['data-type'] ? target.attributes['data-type'].nodeValue : null, target); + }); + $__default['default'](document).on('click', SELECTOR_DATA_TOGGLE_FULLSCREEN, function (e) { + e.preventDefault(); + + _this2.toggleFullscreen(); + }); + var mousedown = false; + var mousedownInterval = null; + $__default['default'](document).on('mousedown', SELECTOR_DATA_TOGGLE_SCROLL_LEFT, function (e) { + e.preventDefault(); + clearInterval(mousedownInterval); + var scrollOffset = _this2._config.scrollOffset; + + if (!_this2._config.scrollBehaviorSwap) { + scrollOffset = -scrollOffset; + } + + mousedown = true; + + _this2._navScroll(scrollOffset); + + mousedownInterval = setInterval(function () { + _this2._navScroll(scrollOffset); + }, 250); + }); + $__default['default'](document).on('mousedown', SELECTOR_DATA_TOGGLE_SCROLL_RIGHT, function (e) { + e.preventDefault(); + clearInterval(mousedownInterval); + var scrollOffset = _this2._config.scrollOffset; + + if (_this2._config.scrollBehaviorSwap) { + scrollOffset = -scrollOffset; + } + + mousedown = true; + + _this2._navScroll(scrollOffset); + + mousedownInterval = setInterval(function () { + _this2._navScroll(scrollOffset); + }, 250); + }); + $__default['default'](document).on('mouseup', function () { + if (mousedown) { + mousedown = false; + clearInterval(mousedownInterval); + mousedownInterval = null; + } + }); + }; + + _proto._setItemActive = function _setItemActive(href) { + $__default['default'](SELECTOR_SIDEBAR_MENU_ITEM + ", " + SELECTOR_HEADER_DROPDOWN_ITEM).removeClass('active'); + $__default['default'](SELECTOR_HEADER_MENU_ITEM).parent().removeClass('active'); + var $headerMenuItem = $__default['default'](SELECTOR_HEADER_MENU_ITEM + "[href$=\"" + href + "\"]"); + var $headerDropdownItem = $__default['default'](SELECTOR_HEADER_DROPDOWN_ITEM + "[href$=\"" + href + "\"]"); + var $sidebarMenuItem = $__default['default'](SELECTOR_SIDEBAR_MENU_ITEM + "[href$=\"" + href + "\"]"); + $headerMenuItem.each(function (i, e) { + $__default['default'](e).parent().addClass('active'); + }); + $headerDropdownItem.each(function (i, e) { + $__default['default'](e).addClass('active'); + }); + $sidebarMenuItem.each(function (i, e) { + $__default['default'](e).addClass('active'); + $__default['default'](e).parents('.nav-treeview').prevAll('.nav-link').addClass('active'); + }); + }; + + _proto._fixHeight = function _fixHeight(tabEmpty) { + if (tabEmpty === void 0) { + tabEmpty = false; + } + + if ($__default['default']('body').hasClass(CLASS_NAME_FULLSCREEN_MODE)) { + var windowHeight = $__default['default'](window).height(); + var navbarHeight = $__default['default'](SELECTOR_TAB_NAV).outerHeight(); + $__default['default'](SELECTOR_TAB_EMPTY + ", " + SELECTOR_TAB_LOADING + ", " + SELECTOR_CONTENT_IFRAME).height(windowHeight - navbarHeight); + $__default['default'](SELECTOR_CONTENT_WRAPPER).height(windowHeight); + } else { + var contentWrapperHeight = parseFloat($__default['default'](SELECTOR_CONTENT_WRAPPER).css('height')); + + var _navbarHeight = $__default['default'](SELECTOR_TAB_NAV).outerHeight(); + + if (tabEmpty == true) { + setTimeout(function () { + $__default['default'](SELECTOR_TAB_EMPTY + ", " + SELECTOR_TAB_LOADING).height(contentWrapperHeight - _navbarHeight); + }, 50); + } else { + $__default['default'](SELECTOR_CONTENT_IFRAME).height(contentWrapperHeight - _navbarHeight); + } + } + } // Static + ; + + IFrame._jQueryInterface = function _jQueryInterface(operation) { + var data = $__default['default'](this).data(DATA_KEY$7); + + var _options = $__default['default'].extend({}, Default$7, $__default['default'](this).data()); + + if (!data) { + data = new IFrame(this, _options); + $__default['default'](this).data(DATA_KEY$7, data); + } + + if (typeof operation === 'string' && /createTab|openTabSidebar|switchTab|removeActiveTab/.test(operation)) { + var _data; + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + (_data = data)[operation].apply(_data, args); + } + }; + + return IFrame; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](window).on('load', function () { + IFrame._jQueryInterface.call($__default['default'](SELECTOR_DATA_TOGGLE$1)); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$7] = IFrame._jQueryInterface; + $__default['default'].fn[NAME$7].Constructor = IFrame; + + $__default['default'].fn[NAME$7].noConflict = function () { + $__default['default'].fn[NAME$7] = JQUERY_NO_CONFLICT$7; + return IFrame._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE Layout.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$6 = 'Layout'; + var DATA_KEY$6 = 'lte.layout'; + var JQUERY_NO_CONFLICT$6 = $__default['default'].fn[NAME$6]; + var SELECTOR_HEADER = '.main-header'; + var SELECTOR_MAIN_SIDEBAR = '.main-sidebar'; + var SELECTOR_SIDEBAR$1 = '.main-sidebar .sidebar'; + var SELECTOR_CONTENT = '.content-wrapper'; + var SELECTOR_CONTROL_SIDEBAR_CONTENT = '.control-sidebar-content'; + var SELECTOR_CONTROL_SIDEBAR_BTN = '[data-widget="control-sidebar"]'; + var SELECTOR_FOOTER = '.main-footer'; + var SELECTOR_PUSHMENU_BTN = '[data-widget="pushmenu"]'; + var SELECTOR_LOGIN_BOX = '.login-box'; + var SELECTOR_REGISTER_BOX = '.register-box'; + var SELECTOR_PRELOADER = '.preloader'; + var CLASS_NAME_SIDEBAR_COLLAPSED$1 = 'sidebar-collapse'; + var CLASS_NAME_SIDEBAR_FOCUSED = 'sidebar-focused'; + var CLASS_NAME_LAYOUT_FIXED = 'layout-fixed'; + var CLASS_NAME_CONTROL_SIDEBAR_SLIDE_OPEN = 'control-sidebar-slide-open'; + var CLASS_NAME_CONTROL_SIDEBAR_OPEN = 'control-sidebar-open'; + var Default$6 = { + scrollbarTheme: 'os-theme-light', + scrollbarAutoHide: 'l', + panelAutoHeight: true, + panelAutoHeightMode: 'min-height', + preloadDuration: 200, + loginRegisterAutoHeight: true + }; + /** + * Class Definition + * ==================================================== + */ + + var Layout = /*#__PURE__*/function () { + function Layout(element, config) { + this._config = config; + this._element = element; + } // Public + + + var _proto = Layout.prototype; + + _proto.fixLayoutHeight = function fixLayoutHeight(extra) { + if (extra === void 0) { + extra = null; + } + + var $body = $__default['default']('body'); + var controlSidebar = 0; + + if ($body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_SLIDE_OPEN) || $body.hasClass(CLASS_NAME_CONTROL_SIDEBAR_OPEN) || extra === 'control_sidebar') { + controlSidebar = $__default['default'](SELECTOR_CONTROL_SIDEBAR_CONTENT).outerHeight(); + } + + var heights = { + window: $__default['default'](window).height(), + header: $__default['default'](SELECTOR_HEADER).length > 0 ? $__default['default'](SELECTOR_HEADER).outerHeight() : 0, + footer: $__default['default'](SELECTOR_FOOTER).length > 0 ? $__default['default'](SELECTOR_FOOTER).outerHeight() : 0, + sidebar: $__default['default'](SELECTOR_SIDEBAR$1).length > 0 ? $__default['default'](SELECTOR_SIDEBAR$1).height() : 0, + controlSidebar: controlSidebar + }; + + var max = this._max(heights); + + var offset = this._config.panelAutoHeight; + + if (offset === true) { + offset = 0; + } + + var $contentSelector = $__default['default'](SELECTOR_CONTENT); + + if (offset !== false) { + if (max === heights.controlSidebar) { + $contentSelector.css(this._config.panelAutoHeightMode, max + offset); + } else if (max === heights.window) { + $contentSelector.css(this._config.panelAutoHeightMode, max + offset - heights.header - heights.footer); + } else { + $contentSelector.css(this._config.panelAutoHeightMode, max + offset - heights.header); + } + + if (this._isFooterFixed()) { + $contentSelector.css(this._config.panelAutoHeightMode, parseFloat($contentSelector.css(this._config.panelAutoHeightMode)) + heights.footer); + } + } + + if (!$body.hasClass(CLASS_NAME_LAYOUT_FIXED)) { + return; + } + + if (typeof $__default['default'].fn.overlayScrollbars !== 'undefined') { + $__default['default'](SELECTOR_SIDEBAR$1).overlayScrollbars({ + className: this._config.scrollbarTheme, + sizeAutoCapable: true, + scrollbars: { + autoHide: this._config.scrollbarAutoHide, + clickScrolling: true + } + }); + } else { + $__default['default'](SELECTOR_SIDEBAR$1).css('overflow-y', 'auto'); + } + }; + + _proto.fixLoginRegisterHeight = function fixLoginRegisterHeight() { + var $body = $__default['default']('body'); + var $selector = $__default['default'](SELECTOR_LOGIN_BOX + ", " + SELECTOR_REGISTER_BOX); + + if ($selector.length === 0) { + $body.css('height', 'auto'); + $__default['default']('html').css('height', 'auto'); + } else { + var boxHeight = $selector.height(); + + if ($body.css(this._config.panelAutoHeightMode) !== boxHeight) { + $body.css(this._config.panelAutoHeightMode, boxHeight); + } + } + } // Private + ; + + _proto._init = function _init() { + var _this = this; + + // Activate layout height watcher + this.fixLayoutHeight(); + + if (this._config.loginRegisterAutoHeight === true) { + this.fixLoginRegisterHeight(); + } else if (this._config.loginRegisterAutoHeight === parseInt(this._config.loginRegisterAutoHeight, 10)) { + setInterval(this.fixLoginRegisterHeight, this._config.loginRegisterAutoHeight); + } + + $__default['default'](SELECTOR_SIDEBAR$1).on('collapsed.lte.treeview expanded.lte.treeview', function () { + _this.fixLayoutHeight(); + }); + $__default['default'](SELECTOR_MAIN_SIDEBAR).on('mouseenter mouseleave', function () { + if ($__default['default']('body').hasClass(CLASS_NAME_SIDEBAR_COLLAPSED$1)) { + _this.fixLayoutHeight(); + } + }); + $__default['default'](SELECTOR_PUSHMENU_BTN).on('collapsed.lte.pushmenu shown.lte.pushmenu', function () { + setTimeout(function () { + _this.fixLayoutHeight(); + }, 300); + }); + $__default['default'](SELECTOR_CONTROL_SIDEBAR_BTN).on('collapsed.lte.controlsidebar', function () { + _this.fixLayoutHeight(); + }).on('expanded.lte.controlsidebar', function () { + _this.fixLayoutHeight('control_sidebar'); + }); + $__default['default'](window).resize(function () { + _this.fixLayoutHeight(); + }); + setTimeout(function () { + $__default['default']('body.hold-transition').removeClass('hold-transition'); + }, 50); + setTimeout(function () { + var $preloader = $__default['default'](SELECTOR_PRELOADER); + + if ($preloader) { + $preloader.css('height', 0); + setTimeout(function () { + $preloader.children().hide(); + }, 200); + } + }, this._config.preloadDuration); + }; + + _proto._max = function _max(numbers) { + // Calculate the maximum number in a list + var max = 0; + Object.keys(numbers).forEach(function (key) { + if (numbers[key] > max) { + max = numbers[key]; + } + }); + return max; + }; + + _proto._isFooterFixed = function _isFooterFixed() { + return $__default['default'](SELECTOR_FOOTER).css('position') === 'fixed'; + } // Static + ; + + Layout._jQueryInterface = function _jQueryInterface(config) { + if (config === void 0) { + config = ''; + } + + return this.each(function () { + var data = $__default['default'](this).data(DATA_KEY$6); + + var _options = $__default['default'].extend({}, Default$6, $__default['default'](this).data()); + + if (!data) { + data = new Layout($__default['default'](this), _options); + $__default['default'](this).data(DATA_KEY$6, data); + } + + if (config === 'init' || config === '') { + data._init(); + } else if (config === 'fixLayoutHeight' || config === 'fixLoginRegisterHeight') { + data[config](); + } + }); + }; + + return Layout; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](window).on('load', function () { + Layout._jQueryInterface.call($__default['default']('body')); + }); + $__default['default'](SELECTOR_SIDEBAR$1 + " a").on('focusin', function () { + $__default['default'](SELECTOR_MAIN_SIDEBAR).addClass(CLASS_NAME_SIDEBAR_FOCUSED); + }).on('focusout', function () { + $__default['default'](SELECTOR_MAIN_SIDEBAR).removeClass(CLASS_NAME_SIDEBAR_FOCUSED); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$6] = Layout._jQueryInterface; + $__default['default'].fn[NAME$6].Constructor = Layout; + + $__default['default'].fn[NAME$6].noConflict = function () { + $__default['default'].fn[NAME$6] = JQUERY_NO_CONFLICT$6; + return Layout._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE PushMenu.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$5 = 'PushMenu'; + var DATA_KEY$5 = 'lte.pushmenu'; + var EVENT_KEY$2 = "." + DATA_KEY$5; + var JQUERY_NO_CONFLICT$5 = $__default['default'].fn[NAME$5]; + var EVENT_COLLAPSED$1 = "collapsed" + EVENT_KEY$2; + var EVENT_SHOWN = "shown" + EVENT_KEY$2; + var SELECTOR_TOGGLE_BUTTON$1 = '[data-widget="pushmenu"]'; + var SELECTOR_BODY = 'body'; + var SELECTOR_OVERLAY = '#sidebar-overlay'; + var SELECTOR_WRAPPER = '.wrapper'; + var CLASS_NAME_COLLAPSED = 'sidebar-collapse'; + var CLASS_NAME_OPEN$3 = 'sidebar-open'; + var CLASS_NAME_IS_OPENING$1 = 'sidebar-is-opening'; + var CLASS_NAME_CLOSED = 'sidebar-closed'; + var Default$5 = { + autoCollapseSize: 992, + enableRemember: false, + noTransitionAfterReload: true + }; + /** + * Class Definition + * ==================================================== + */ + + var PushMenu = /*#__PURE__*/function () { + function PushMenu(element, options) { + this._element = element; + this._options = $__default['default'].extend({}, Default$5, options); + + if ($__default['default'](SELECTOR_OVERLAY).length === 0) { + this._addOverlay(); + } + + this._init(); + } // Public + + + var _proto = PushMenu.prototype; + + _proto.expand = function expand() { + var $bodySelector = $__default['default'](SELECTOR_BODY); + + if (this._options.autoCollapseSize && $__default['default'](window).width() <= this._options.autoCollapseSize) { + $bodySelector.addClass(CLASS_NAME_OPEN$3); + } + + $bodySelector.addClass(CLASS_NAME_IS_OPENING$1).removeClass(CLASS_NAME_COLLAPSED + " " + CLASS_NAME_CLOSED).delay(50).queue(function () { + $bodySelector.removeClass(CLASS_NAME_IS_OPENING$1); + $__default['default'](this).dequeue(); + }); + + if (this._options.enableRemember) { + localStorage.setItem("remember" + EVENT_KEY$2, CLASS_NAME_OPEN$3); + } + + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_SHOWN)); + }; + + _proto.collapse = function collapse() { + var $bodySelector = $__default['default'](SELECTOR_BODY); + + if (this._options.autoCollapseSize && $__default['default'](window).width() <= this._options.autoCollapseSize) { + $bodySelector.removeClass(CLASS_NAME_OPEN$3).addClass(CLASS_NAME_CLOSED); + } + + $bodySelector.addClass(CLASS_NAME_COLLAPSED); + + if (this._options.enableRemember) { + localStorage.setItem("remember" + EVENT_KEY$2, CLASS_NAME_COLLAPSED); + } + + $__default['default'](this._element).trigger($__default['default'].Event(EVENT_COLLAPSED$1)); + }; + + _proto.toggle = function toggle() { + if ($__default['default'](SELECTOR_BODY).hasClass(CLASS_NAME_COLLAPSED)) { + this.expand(); + } else { + this.collapse(); + } + }; + + _proto.autoCollapse = function autoCollapse(resize) { + if (resize === void 0) { + resize = false; + } + + if (!this._options.autoCollapseSize) { + return; + } + + var $bodySelector = $__default['default'](SELECTOR_BODY); + + if ($__default['default'](window).width() <= this._options.autoCollapseSize) { + if (!$bodySelector.hasClass(CLASS_NAME_OPEN$3)) { + this.collapse(); + } + } else if (resize === true) { + if ($bodySelector.hasClass(CLASS_NAME_OPEN$3)) { + $bodySelector.removeClass(CLASS_NAME_OPEN$3); + } else if ($bodySelector.hasClass(CLASS_NAME_CLOSED)) { + this.expand(); + } + } + }; + + _proto.remember = function remember() { + if (!this._options.enableRemember) { + return; + } + + var $body = $__default['default']('body'); + var toggleState = localStorage.getItem("remember" + EVENT_KEY$2); + + if (toggleState === CLASS_NAME_COLLAPSED) { + if (this._options.noTransitionAfterReload) { + $body.addClass('hold-transition').addClass(CLASS_NAME_COLLAPSED).delay(50).queue(function () { + $__default['default'](this).removeClass('hold-transition'); + $__default['default'](this).dequeue(); + }); + } else { + $body.addClass(CLASS_NAME_COLLAPSED); + } + } else if (this._options.noTransitionAfterReload) { + $body.addClass('hold-transition').removeClass(CLASS_NAME_COLLAPSED).delay(50).queue(function () { + $__default['default'](this).removeClass('hold-transition'); + $__default['default'](this).dequeue(); + }); + } else { + $body.removeClass(CLASS_NAME_COLLAPSED); + } + } // Private + ; + + _proto._init = function _init() { + var _this = this; + + this.remember(); + this.autoCollapse(); + $__default['default'](window).resize(function () { + _this.autoCollapse(true); + }); + }; + + _proto._addOverlay = function _addOverlay() { + var _this2 = this; + + var overlay = $__default['default']('
    ', { + id: 'sidebar-overlay' + }); + overlay.on('click', function () { + _this2.collapse(); + }); + $__default['default'](SELECTOR_WRAPPER).append(overlay); + } // Static + ; + + PushMenu._jQueryInterface = function _jQueryInterface(operation) { + return this.each(function () { + var data = $__default['default'](this).data(DATA_KEY$5); + + var _options = $__default['default'].extend({}, Default$5, $__default['default'](this).data()); + + if (!data) { + data = new PushMenu(this, _options); + $__default['default'](this).data(DATA_KEY$5, data); + } + + if (typeof operation === 'string' && /collapse|expand|toggle/.test(operation)) { + data[operation](); + } + }); + }; + + return PushMenu; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_TOGGLE_BUTTON$1, function (event) { + event.preventDefault(); + var button = event.currentTarget; + + if ($__default['default'](button).data('widget') !== 'pushmenu') { + button = $__default['default'](button).closest(SELECTOR_TOGGLE_BUTTON$1); + } + + PushMenu._jQueryInterface.call($__default['default'](button), 'toggle'); + }); + $__default['default'](window).on('load', function () { + PushMenu._jQueryInterface.call($__default['default'](SELECTOR_TOGGLE_BUTTON$1)); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$5] = PushMenu._jQueryInterface; + $__default['default'].fn[NAME$5].Constructor = PushMenu; + + $__default['default'].fn[NAME$5].noConflict = function () { + $__default['default'].fn[NAME$5] = JQUERY_NO_CONFLICT$5; + return PushMenu._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE SidebarSearch.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$4 = 'SidebarSearch'; + var DATA_KEY$4 = 'lte.sidebar-search'; + var JQUERY_NO_CONFLICT$4 = $__default['default'].fn[NAME$4]; + var CLASS_NAME_OPEN$2 = 'sidebar-search-open'; + var CLASS_NAME_ICON_SEARCH = 'fa-search'; + var CLASS_NAME_ICON_CLOSE = 'fa-times'; + var CLASS_NAME_HEADER = 'nav-header'; + var CLASS_NAME_SEARCH_RESULTS = 'sidebar-search-results'; + var CLASS_NAME_LIST_GROUP = 'list-group'; + var SELECTOR_DATA_WIDGET$1 = '[data-widget="sidebar-search"]'; + var SELECTOR_SIDEBAR = '.main-sidebar .nav-sidebar'; + var SELECTOR_NAV_LINK = '.nav-link'; + var SELECTOR_NAV_TREEVIEW = '.nav-treeview'; + var SELECTOR_SEARCH_INPUT$1 = SELECTOR_DATA_WIDGET$1 + " .form-control"; + var SELECTOR_SEARCH_BUTTON = SELECTOR_DATA_WIDGET$1 + " .btn"; + var SELECTOR_SEARCH_ICON = SELECTOR_SEARCH_BUTTON + " i"; + var SELECTOR_SEARCH_LIST_GROUP = "." + CLASS_NAME_LIST_GROUP; + var SELECTOR_SEARCH_RESULTS = "." + CLASS_NAME_SEARCH_RESULTS; + var SELECTOR_SEARCH_RESULTS_GROUP = SELECTOR_SEARCH_RESULTS + " ." + CLASS_NAME_LIST_GROUP; + var Default$4 = { + arrowSign: '->', + minLength: 3, + maxResults: 7, + highlightName: true, + highlightPath: false, + highlightClass: 'text-light', + notFoundText: 'No element found!' + }; + var SearchItems = []; + /** + * Class Definition + * ==================================================== + */ + + var SidebarSearch = /*#__PURE__*/function () { + function SidebarSearch(_element, _options) { + this.element = _element; + this.options = $__default['default'].extend({}, Default$4, _options); + this.items = []; + } // Public + + + var _proto = SidebarSearch.prototype; + + _proto.init = function init() { + var _this = this; + + if ($__default['default'](SELECTOR_DATA_WIDGET$1).length === 0) { + return; + } + + if ($__default['default'](SELECTOR_DATA_WIDGET$1).next(SELECTOR_SEARCH_RESULTS).length === 0) { + $__default['default'](SELECTOR_DATA_WIDGET$1).after($__default['default']('
    ', { + class: CLASS_NAME_SEARCH_RESULTS + })); + } + + if ($__default['default'](SELECTOR_SEARCH_RESULTS).children(SELECTOR_SEARCH_LIST_GROUP).length === 0) { + $__default['default'](SELECTOR_SEARCH_RESULTS).append($__default['default']('
    ', { + class: CLASS_NAME_LIST_GROUP + })); + } + + this._addNotFound(); + + $__default['default'](SELECTOR_SIDEBAR).children().each(function (i, child) { + _this._parseItem(child); + }); + }; + + _proto.search = function search() { + var _this2 = this; + + var searchValue = $__default['default'](SELECTOR_SEARCH_INPUT$1).val().toLowerCase(); + + if (searchValue.length < this.options.minLength) { + $__default['default'](SELECTOR_SEARCH_RESULTS_GROUP).empty(); + + this._addNotFound(); + + this.close(); + return; + } + + var searchResults = SearchItems.filter(function (item) { + return item.name.toLowerCase().includes(searchValue); + }); + var endResults = $__default['default'](searchResults.slice(0, this.options.maxResults)); + $__default['default'](SELECTOR_SEARCH_RESULTS_GROUP).empty(); + + if (endResults.length === 0) { + this._addNotFound(); + } else { + endResults.each(function (i, result) { + $__default['default'](SELECTOR_SEARCH_RESULTS_GROUP).append(_this2._renderItem(escape(result.name), escape(result.link), result.path)); + }); + } + + this.open(); + }; + + _proto.open = function open() { + $__default['default'](SELECTOR_DATA_WIDGET$1).parent().addClass(CLASS_NAME_OPEN$2); + $__default['default'](SELECTOR_SEARCH_ICON).removeClass(CLASS_NAME_ICON_SEARCH).addClass(CLASS_NAME_ICON_CLOSE); + }; + + _proto.close = function close() { + $__default['default'](SELECTOR_DATA_WIDGET$1).parent().removeClass(CLASS_NAME_OPEN$2); + $__default['default'](SELECTOR_SEARCH_ICON).removeClass(CLASS_NAME_ICON_CLOSE).addClass(CLASS_NAME_ICON_SEARCH); + }; + + _proto.toggle = function toggle() { + if ($__default['default'](SELECTOR_DATA_WIDGET$1).parent().hasClass(CLASS_NAME_OPEN$2)) { + this.close(); + } else { + this.open(); + } + } // Private + ; + + _proto._parseItem = function _parseItem(item, path) { + var _this3 = this; + + if (path === void 0) { + path = []; + } + + if ($__default['default'](item).hasClass(CLASS_NAME_HEADER)) { + return; + } + + var itemObject = {}; + var navLink = $__default['default'](item).clone().find("> " + SELECTOR_NAV_LINK); + var navTreeview = $__default['default'](item).clone().find("> " + SELECTOR_NAV_TREEVIEW); + var link = navLink.attr('href'); + var name = navLink.find('p').children().remove().end().text(); + itemObject.name = this._trimText(name); + itemObject.link = link; + itemObject.path = path; + + if (navTreeview.length === 0) { + SearchItems.push(itemObject); + } else { + var newPath = itemObject.path.concat([itemObject.name]); + navTreeview.children().each(function (i, child) { + _this3._parseItem(child, newPath); + }); + } + }; + + _proto._trimText = function _trimText(text) { + return $.trim(text.replace(/(\r\n|\n|\r)/gm, ' ')); + }; + + _proto._renderItem = function _renderItem(name, link, path) { + var _this4 = this; + + path = path.join(" " + this.options.arrowSign + " "); + name = unescape(name); + + if (this.options.highlightName || this.options.highlightPath) { + var searchValue = $__default['default'](SELECTOR_SEARCH_INPUT$1).val().toLowerCase(); + var regExp = new RegExp(searchValue, 'gi'); + + if (this.options.highlightName) { + name = name.replace(regExp, function (str) { + return "" + str + ""; + }); + } + + if (this.options.highlightPath) { + path = path.replace(regExp, function (str) { + return "" + str + ""; + }); + } + } + + var groupItemElement = $__default['default']('', { + href: link, + class: 'list-group-item' + }); + var searchTitleElement = $__default['default']('
    ', { + class: 'search-title' + }).html(name); + var searchPathElement = $__default['default']('
    ', { + class: 'search-path' + }).html(path); + groupItemElement.append(searchTitleElement).append(searchPathElement); + return groupItemElement; + }; + + _proto._addNotFound = function _addNotFound() { + $__default['default'](SELECTOR_SEARCH_RESULTS_GROUP).append(this._renderItem(this.options.notFoundText, '#', [])); + } // Static + ; + + SidebarSearch._jQueryInterface = function _jQueryInterface(config) { + var data = $__default['default'](this).data(DATA_KEY$4); + + if (!data) { + data = $__default['default'](this).data(); + } + + var _options = $__default['default'].extend({}, Default$4, typeof config === 'object' ? config : data); + + var plugin = new SidebarSearch($__default['default'](this), _options); + $__default['default'](this).data(DATA_KEY$4, typeof config === 'object' ? config : data); + + if (typeof config === 'string' && /init|toggle|close|open|search/.test(config)) { + plugin[config](); + } else { + plugin.init(); + } + }; + + return SidebarSearch; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_SEARCH_BUTTON, function (event) { + event.preventDefault(); + + SidebarSearch._jQueryInterface.call($__default['default'](SELECTOR_DATA_WIDGET$1), 'toggle'); + }); + $__default['default'](document).on('keyup', SELECTOR_SEARCH_INPUT$1, function (event) { + if (event.keyCode == 38) { + event.preventDefault(); + $__default['default'](SELECTOR_SEARCH_RESULTS_GROUP).children().last().focus(); + return; + } + + if (event.keyCode == 40) { + event.preventDefault(); + $__default['default'](SELECTOR_SEARCH_RESULTS_GROUP).children().first().focus(); + return; + } + + setTimeout(function () { + SidebarSearch._jQueryInterface.call($__default['default'](SELECTOR_DATA_WIDGET$1), 'search'); + }, 100); + }); + $__default['default'](document).on('keydown', SELECTOR_SEARCH_RESULTS_GROUP, function (event) { + var $focused = $__default['default'](':focus'); + + if (event.keyCode == 38) { + event.preventDefault(); + + if ($focused.is(':first-child')) { + $focused.siblings().last().focus(); + } else { + $focused.prev().focus(); + } + } + + if (event.keyCode == 40) { + event.preventDefault(); + + if ($focused.is(':last-child')) { + $focused.siblings().first().focus(); + } else { + $focused.next().focus(); + } + } + }); + $__default['default'](window).on('load', function () { + SidebarSearch._jQueryInterface.call($__default['default'](SELECTOR_DATA_WIDGET$1), 'init'); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$4] = SidebarSearch._jQueryInterface; + $__default['default'].fn[NAME$4].Constructor = SidebarSearch; + + $__default['default'].fn[NAME$4].noConflict = function () { + $__default['default'].fn[NAME$4] = JQUERY_NO_CONFLICT$4; + return SidebarSearch._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE NavbarSearch.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$3 = 'NavbarSearch'; + var DATA_KEY$3 = 'lte.navbar-search'; + var JQUERY_NO_CONFLICT$3 = $__default['default'].fn[NAME$3]; + var SELECTOR_TOGGLE_BUTTON = '[data-widget="navbar-search"]'; + var SELECTOR_SEARCH_BLOCK = '.navbar-search-block'; + var SELECTOR_SEARCH_INPUT = '.form-control'; + var CLASS_NAME_OPEN$1 = 'navbar-search-open'; + var Default$3 = { + resetOnClose: true, + target: SELECTOR_SEARCH_BLOCK + }; + /** + * Class Definition + * ==================================================== + */ + + var NavbarSearch = /*#__PURE__*/function () { + function NavbarSearch(_element, _options) { + this._element = _element; + this._config = $__default['default'].extend({}, Default$3, _options); + } // Public + + + var _proto = NavbarSearch.prototype; + + _proto.open = function open() { + $__default['default'](this._config.target).css('display', 'flex').hide().fadeIn().addClass(CLASS_NAME_OPEN$1); + $__default['default'](this._config.target + " " + SELECTOR_SEARCH_INPUT).focus(); + }; + + _proto.close = function close() { + $__default['default'](this._config.target).fadeOut().removeClass(CLASS_NAME_OPEN$1); + + if (this._config.resetOnClose) { + $__default['default'](this._config.target + " " + SELECTOR_SEARCH_INPUT).val(''); + } + }; + + _proto.toggle = function toggle() { + if ($__default['default'](this._config.target).hasClass(CLASS_NAME_OPEN$1)) { + this.close(); + } else { + this.open(); + } + } // Static + ; + + NavbarSearch._jQueryInterface = function _jQueryInterface(options) { + return this.each(function () { + var data = $__default['default'](this).data(DATA_KEY$3); + + var _options = $__default['default'].extend({}, Default$3, $__default['default'](this).data()); + + if (!data) { + data = new NavbarSearch(this, _options); + $__default['default'](this).data(DATA_KEY$3, data); + } + + if (!/toggle|close|open/.test(options)) { + throw new Error("Undefined method " + options); + } + + data[options](); + }); + }; + + return NavbarSearch; + }(); + /** + * Data API + * ==================================================== + */ + + + $__default['default'](document).on('click', SELECTOR_TOGGLE_BUTTON, function (event) { + event.preventDefault(); + var button = $__default['default'](event.currentTarget); + + if (button.data('widget') !== 'navbar-search') { + button = button.closest(SELECTOR_TOGGLE_BUTTON); + } + + NavbarSearch._jQueryInterface.call(button, 'toggle'); + }); + /** + * jQuery API + * ==================================================== + */ + + $__default['default'].fn[NAME$3] = NavbarSearch._jQueryInterface; + $__default['default'].fn[NAME$3].Constructor = NavbarSearch; + + $__default['default'].fn[NAME$3].noConflict = function () { + $__default['default'].fn[NAME$3] = JQUERY_NO_CONFLICT$3; + return NavbarSearch._jQueryInterface; + }; + + /** + * -------------------------------------------- + * AdminLTE Toasts.js + * License MIT + * -------------------------------------------- + */ + /** + * Constants + * ==================================================== + */ + + var NAME$2 = 'Toasts'; + var DATA_KEY$2 = 'lte.toasts'; + var EVENT_KEY$1 = "." + DATA_KEY$2; + var JQUERY_NO_CONFLICT$2 = $__default['default'].fn[NAME$2]; + var EVENT_INIT = "init" + EVENT_KEY$1; + var EVENT_CREATED = "created" + EVENT_KEY$1; + var EVENT_REMOVED = "removed" + EVENT_KEY$1; + var SELECTOR_CONTAINER_TOP_RIGHT = '#toastsContainerTopRight'; + var SELECTOR_CONTAINER_TOP_LEFT = '#toastsContainerTopLeft'; + var SELECTOR_CONTAINER_BOTTOM_RIGHT = '#toastsContainerBottomRight'; + var SELECTOR_CONTAINER_BOTTOM_LEFT = '#toastsContainerBottomLeft'; + var CLASS_NAME_TOP_RIGHT = 'toasts-top-right'; + var CLASS_NAME_TOP_LEFT = 'toasts-top-left'; + var CLASS_NAME_BOTTOM_RIGHT = 'toasts-bottom-right'; + var CLASS_NAME_BOTTOM_LEFT = 'toasts-bottom-left'; + var POSITION_TOP_RIGHT = 'topRight'; + var POSITION_TOP_LEFT = 'topLeft'; + var POSITION_BOTTOM_RIGHT = 'bottomRight'; + var POSITION_BOTTOM_LEFT = 'bottomLeft'; + var Default$2 = { + position: POSITION_TOP_RIGHT, + fixed: true, + autohide: false, + autoremove: true, + delay: 1000, + fade: true, + icon: null, + image: null, + imageAlt: null, + imageHeight: '25px', + title: null, + subtitle: null, + close: true, + body: null, + class: null + }; + /** + * Class Definition + * ==================================================== + */ + + var Toasts = /*#__PURE__*/function () { + function Toasts(element, config) { + this._config = config; + + this._prepareContainer(); + + $__default['default']('body').trigger($__default['default'].Event(EVENT_INIT)); + } // Public + + + var _proto = Toasts.prototype; + + _proto.create = function create() { + var toast = $__default['default'](' + + +
    + +
    + +
    + + + + + + diff --git a/ems-core/web-admin/src/App.vue b/ems-core/web-admin/src/App.vue new file mode 100644 index 0000000..ce2d97c --- /dev/null +++ b/ems-core/web-admin/src/App.vue @@ -0,0 +1,129 @@ + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/assets/img/backgrounds/circuit-board-5907811_1920-bw.png b/ems-core/web-admin/src/assets/img/backgrounds/circuit-board-5907811_1920-bw.png new file mode 100644 index 0000000..683e378 Binary files /dev/null and b/ems-core/web-admin/src/assets/img/backgrounds/circuit-board-5907811_1920-bw.png differ diff --git a/ems-core/web-admin/src/assets/img/ems-logo-192x192.png b/ems-core/web-admin/src/assets/img/ems-logo-192x192.png new file mode 100644 index 0000000..66bfa78 Binary files /dev/null and b/ems-core/web-admin/src/assets/img/ems-logo-192x192.png differ diff --git a/ems-core/web-admin/src/assets/img/ems-logo.png b/ems-core/web-admin/src/assets/img/ems-logo.png new file mode 100644 index 0000000..bbf2dcf Binary files /dev/null and b/ems-core/web-admin/src/assets/img/ems-logo.png differ diff --git a/ems-core/web-admin/src/assets/img/ems-logo.svg b/ems-core/web-admin/src/assets/img/ems-logo.svg new file mode 100644 index 0000000..e953f79 --- /dev/null +++ b/ems-core/web-admin/src/assets/img/ems-logo.svg @@ -0,0 +1 @@ +EM S \ No newline at end of file diff --git a/ems-core/web-admin/src/assets/img/party.gif b/ems-core/web-admin/src/assets/img/party.gif new file mode 100644 index 0000000..3f3c3f0 Binary files /dev/null and b/ems-core/web-admin/src/assets/img/party.gif differ diff --git a/ems-core/web-admin/src/components/7seg/7seg.vue b/ems-core/web-admin/src/components/7seg/7seg.vue new file mode 100644 index 0000000..95362c5 --- /dev/null +++ b/ems-core/web-admin/src/components/7seg/7seg.vue @@ -0,0 +1,127 @@ + + + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Bold.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Bold.woff2 new file mode 100644 index 0000000..fb86d7a Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Bold.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-BoldItalic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-BoldItalic.woff2 new file mode 100644 index 0000000..b828392 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-BoldItalic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Italic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Italic.woff2 new file mode 100644 index 0000000..4271825 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Italic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Regular.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Regular.woff2 new file mode 100644 index 0000000..ba7cd02 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Classic/DSEG14Classic-Regular.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Bold.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Bold.woff2 new file mode 100644 index 0000000..c6a6a58 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Bold.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-BoldItalic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-BoldItalic.woff2 new file mode 100644 index 0000000..8731392 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-BoldItalic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Italic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Italic.woff2 new file mode 100644 index 0000000..25c3cf1 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Italic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Regular.woff2 b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Regular.woff2 new file mode 100644 index 0000000..2240b81 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG14-Modern/DSEG14Modern-Regular.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Bold.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Bold.woff2 new file mode 100644 index 0000000..558eec4 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Bold.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-BoldItalic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-BoldItalic.woff2 new file mode 100644 index 0000000..096a08a Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-BoldItalic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Italic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Italic.woff2 new file mode 100644 index 0000000..937675e Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Italic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Regular.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Regular.woff2 new file mode 100644 index 0000000..ff29060 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Classic/DSEG7Classic-Regular.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Bold.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Bold.woff2 new file mode 100644 index 0000000..1aa2789 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Bold.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-BoldItalic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-BoldItalic.woff2 new file mode 100644 index 0000000..7c1c1a6 Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-BoldItalic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Italic.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Italic.woff2 new file mode 100644 index 0000000..c643f2c Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Italic.woff2 differ diff --git a/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Regular.woff2 b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Regular.woff2 new file mode 100644 index 0000000..0075dbe Binary files /dev/null and b/ems-core/web-admin/src/components/7seg/DSEG7-Modern/DSEG7Modern-Regular.woff2 differ diff --git a/ems-core/web-admin/src/components/ace-editor/ace-editor.vue b/ems-core/web-admin/src/components/ace-editor/ace-editor.vue new file mode 100644 index 0000000..9403d77 --- /dev/null +++ b/ems-core/web-admin/src/components/ace-editor/ace-editor.vue @@ -0,0 +1,177 @@ + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/card/card.html b/ems-core/web-admin/src/components/card/card.html new file mode 100644 index 0000000..f459922 --- /dev/null +++ b/ems-core/web-admin/src/components/card/card.html @@ -0,0 +1,40 @@ +
    +
    + +
    + +
    +
    {{header}}
    +
    +
    + + + + + +
    +
    + +
    + +
    {{title}}
    +
    + +

    + +

    + {{l.text}} + +
    + + + +
    diff --git a/ems-core/web-admin/src/components/card/card.js b/ems-core/web-admin/src/components/card/card.js new file mode 100644 index 0000000..5701b6f --- /dev/null +++ b/ems-core/web-admin/src/components/card/card.js @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +export default { + name: 'card', + props: { + id: String, + classes: String, + style: String, + bodyClasses: String, + header: String, + icon: String, + title: String, + footer: String, + links: Object, + hasRefresh: Boolean, + hasCollapse: Boolean, + hasMaximize: Boolean, + hasRemove: Boolean, + runRefresh: String + }, + methods: { + doRefresh() { + if (this.runRefresh && this.runRefresh!=='') + eval(this.runRefresh); // Can access local scope (and variables) + //new Function( 'return (' + this.runRefresh + ')' )(); // Cannot access local scope (and variables) + } + } +} diff --git a/ems-core/web-admin/src/components/card/card.vue b/ems-core/web-admin/src/components/card/card.vue new file mode 100644 index 0000000..32d0596 --- /dev/null +++ b/ems-core/web-admin/src/components/card/card.vue @@ -0,0 +1,11 @@ + + + + diff --git a/ems-core/web-admin/src/components/chartjs/chartjs.vue b/ems-core/web-admin/src/components/chartjs/chartjs.vue new file mode 100644 index 0000000..1752cab --- /dev/null +++ b/ems-core/web-admin/src/components/chartjs/chartjs.vue @@ -0,0 +1,147 @@ + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/clock/clock.vue b/ems-core/web-admin/src/components/clock/clock.vue new file mode 100644 index 0000000..ce46cbd --- /dev/null +++ b/ems-core/web-admin/src/components/clock/clock.vue @@ -0,0 +1,45 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/ems/sse/ems-sse.vue b/ems-core/web-admin/src/components/ems/sse/ems-sse.vue new file mode 100644 index 0000000..f2d5bec --- /dev/null +++ b/ems-core/web-admin/src/components/ems/sse/ems-sse.vue @@ -0,0 +1,377 @@ + + + + diff --git a/ems-core/web-admin/src/components/ems/ts/ts.js b/ems-core/web-admin/src/components/ems/ts/ts.js new file mode 100644 index 0000000..f9007b1 --- /dev/null +++ b/ems-core/web-admin/src/components/ems/ts/ts.js @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +export class Timeseries { + #tsLength; + #tsData; + #seqLast; + #allowUpdate = true; + + constructor(l) { + if (!l || typeof l!=='number' || l<1) throw 'Timeseries.init(): Missing or invalid length argument'; + this.tsLength = l; + this.tsData = new Array(l); + this.tsData.fill(null); + this.seqLast = undefined; + } + + getLength() { return this.tsLength; } + + add(data, seq) { + if (!data) return; + if (!seq) { + if (this.seqLast===undefined) seq = 0; + else seq = this.seqLast + 1; + } + if (this.seqLast===undefined) { + this.seqLast = seq; + this.tsData.shift(); + this.tsData.push(data); + return; + } + if (seq <= this.seqLast-this.tsLength) + return; + if (seq<=this.seqLast && this.allowUpdate) { + let offset = this.tsLength - (this.seqLast-seq) - 1; + this.tsData[offset] = data; + } + if (seq > this.seqLast) { + let extra = seq - this.seqLast; + for (let i=0; i=this.tsLength) throw 'Timeseries.getData(): Length argument exceeds size: length='+l+', size='+this.tsLength; + let from = this.tsLength - l; + return this.tsData.slice(from); + } + + getLast() { + return (this.seqLast) ? this.tsData.at(-1) : null; + } + + getDataWithSeq(l) { + let s = this.seqLast - l + 1; + let result = this.getData(l).map(data => ({data, seq: s++})); + return result; + } +} + +export class TimeWindow extends Timeseries { + #winLength; + #winInterval; + + constructor(dur, ival) { + let l = Math.floor(dur / ival) + 1; + super(l); + this.winLength = dur; + this.winInterval = ival; + } + + getWindowLength() { return this.winLength; } + getWindowInterval() { return this.winInterval; } + + getWindowData(dur) { + let l = Math.floor(dur / this.winInterval) + 1; + return super.getData(l); + } +} diff --git a/ems-core/web-admin/src/components/infobox/infobox.html b/ems-core/web-admin/src/components/infobox/infobox.html new file mode 100644 index 0000000..7e73101 --- /dev/null +++ b/ems-core/web-admin/src/components/infobox/infobox.html @@ -0,0 +1,13 @@ +
    + +
    + {{message}} + {{text_before}}{{value ?? text_default}}{{text_after}} +
    + +
    + +
    + +
    + diff --git a/ems-core/web-admin/src/components/infobox/infobox.js b/ems-core/web-admin/src/components/infobox/infobox.js new file mode 100644 index 0000000..fc43c4c --- /dev/null +++ b/ems-core/web-admin/src/components/infobox/infobox.js @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +export default { + name: 'infobox', + props: { + value: String, + id: String, + classes: String, + message: String, + text_before: String, + text_default: String, + text_after: String, + bg_class: String, + icon_classes: String, + loading: Boolean, + style: { type: String, default: 'padding:0;' } + } +} diff --git a/ems-core/web-admin/src/components/infobox/infobox.vue b/ems-core/web-admin/src/components/infobox/infobox.vue new file mode 100644 index 0000000..7123daf --- /dev/null +++ b/ems-core/web-admin/src/components/infobox/infobox.vue @@ -0,0 +1,11 @@ + + + + diff --git a/ems-core/web-admin/src/components/jqvmap/jqvmap.vue b/ems-core/web-admin/src/components/jqvmap/jqvmap.vue new file mode 100644 index 0000000..518177d --- /dev/null +++ b/ems-core/web-admin/src/components/jqvmap/jqvmap.vue @@ -0,0 +1,77 @@ + + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/jvectormap/jvectormap.vue b/ems-core/web-admin/src/components/jvectormap/jvectormap.vue new file mode 100644 index 0000000..9f104df --- /dev/null +++ b/ems-core/web-admin/src/components/jvectormap/jvectormap.vue @@ -0,0 +1,166 @@ + + + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/knob/knob.vue b/ems-core/web-admin/src/components/knob/knob.vue new file mode 100644 index 0000000..a25cad5 --- /dev/null +++ b/ems-core/web-admin/src/components/knob/knob.vue @@ -0,0 +1,103 @@ + + + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/leaflet-map/leaflet-map.vue b/ems-core/web-admin/src/components/leaflet-map/leaflet-map.vue new file mode 100644 index 0000000..ff0a110 --- /dev/null +++ b/ems-core/web-admin/src/components/leaflet-map/leaflet-map.vue @@ -0,0 +1,322 @@ + + + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/modal/modal.vue b/ems-core/web-admin/src/components/modal/modal.vue new file mode 100644 index 0000000..964043c --- /dev/null +++ b/ems-core/web-admin/src/components/modal/modal.vue @@ -0,0 +1,151 @@ + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/smallbox/smallbox.html b/ems-core/web-admin/src/components/smallbox/smallbox.html new file mode 100644 index 0000000..e85bb54 --- /dev/null +++ b/ems-core/web-admin/src/components/smallbox/smallbox.html @@ -0,0 +1,17 @@ + +
    +
    +

    + +

    +

    + +

    +
    +
    + +
    + + More info + +
    diff --git a/ems-core/web-admin/src/components/smallbox/smallbox.js b/ems-core/web-admin/src/components/smallbox/smallbox.js new file mode 100644 index 0000000..d55ec6b --- /dev/null +++ b/ems-core/web-admin/src/components/smallbox/smallbox.js @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +export default { + name: 'smallbox', + props: { + id: String, + init_content: String, + more_info_url: String, + bg_class: String, + icon_classes: String + } +} diff --git a/ems-core/web-admin/src/components/smallbox/smallbox.vue b/ems-core/web-admin/src/components/smallbox/smallbox.vue new file mode 100644 index 0000000..803a6e7 --- /dev/null +++ b/ems-core/web-admin/src/components/smallbox/smallbox.vue @@ -0,0 +1,11 @@ + + + + diff --git a/ems-core/web-admin/src/components/sparkline/sparkline.vue b/ems-core/web-admin/src/components/sparkline/sparkline.vue new file mode 100644 index 0000000..45a7f56 --- /dev/null +++ b/ems-core/web-admin/src/components/sparkline/sparkline.vue @@ -0,0 +1,78 @@ + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/status-led/status-led.vue b/ems-core/web-admin/src/components/status-led/status-led.vue new file mode 100644 index 0000000..64b4c7f --- /dev/null +++ b/ems-core/web-admin/src/components/status-led/status-led.vue @@ -0,0 +1,194 @@ + + + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/worldmap/WorldMap.vue b/ems-core/web-admin/src/components/worldmap/WorldMap.vue new file mode 100644 index 0000000..658cb4d --- /dev/null +++ b/ems-core/web-admin/src/components/worldmap/WorldMap.vue @@ -0,0 +1,65 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/components/worldmap/countries.js b/ems-core/web-admin/src/components/worldmap/countries.js new file mode 100644 index 0000000..2206073 --- /dev/null +++ b/ems-core/web-admin/src/components/worldmap/countries.js @@ -0,0 +1,246 @@ +module.exports = [ + { name: "Afghanistan", code: "AF" }, + { name: "land Islands", code: "AX" }, + { name: "Albania", code: "AL" }, + { name: "Algeria", code: "DZ" }, + { name: "American Samoa", code: "AS" }, + { name: "AndorrA", code: "AD" }, + { name: "Angola", code: "AO" }, + { name: "Anguilla", code: "AI" }, + { name: "Antarctica", code: "AQ" }, + { name: "Antigua and Barbuda", code: "AG" }, + { name: "Argentina", code: "AR" }, + { name: "Armenia", code: "AM" }, + { name: "Aruba", code: "AW" }, + { name: "Australia", code: "AU" }, + { name: "Austria", code: "AT" }, + { name: "Azerbaijan", code: "AZ" }, + { name: "Bahamas", code: "BS" }, + { name: "Bahrain", code: "BH" }, + { name: "Bangladesh", code: "BD" }, + { name: "Barbados", code: "BB" }, + { name: "Belarus", code: "BY" }, + { name: "Belgium", code: "BE" }, + { name: "Belize", code: "BZ" }, + { name: "Benin", code: "BJ" }, + { name: "Bermuda", code: "BM" }, + { name: "Bhutan", code: "BT" }, + { name: "Bolivia", code: "BO" }, + { name: "Bosnia and Herzegovina", code: "BA" }, + { name: "Botswana", code: "BW" }, + { name: "Bouvet Island", code: "BV" }, + { name: "Brazil", code: "BR" }, + { name: "British Indian Ocean Territory", code: "IO" }, + { name: "Brunei Darussalam", code: "BN" }, + { name: "Bulgaria", code: "BG" }, + { name: "Burkina Faso", code: "BF" }, + { name: "Burundi", code: "BI" }, + { name: "Cambodia", code: "KH" }, + { name: "Cameroon", code: "CM" }, + { name: "Canada", code: "CA" }, + { name: "Cape Verde", code: "CV" }, + { name: "Cayman Islands", code: "KY" }, + { name: "Central African Republic", code: "CF" }, + { name: "Chad", code: "TD" }, + { name: "Chile", code: "CL" }, + { name: "China", code: "CN" }, + { name: "Christmas Island", code: "CX" }, + { name: "Cocos (Keeling) Islands", code: "CC" }, + { name: "Colombia", code: "CO" }, + { name: "Comoros", code: "KM" }, + { name: "Congo", code: "CG" }, + { name: "Congo, The Democratic Republic of the", code: "CD" }, + { name: "Cook Islands", code: "CK" }, + { name: "Costa Rica", code: "CR" }, + { name: "Cote D'Ivoire", code: "CI" }, + { name: "Croatia", code: "HR" }, + { name: "Cuba", code: "CU" }, + { name: "Cyprus", code: "CY" }, + { name: "Czech Republic", code: "CZ" }, + { name: "Denmark", code: "DK" }, + { name: "Djibouti", code: "DJ" }, + { name: "Dominica", code: "DM" }, + { name: "Dominican Republic", code: "DO" }, + { name: "Ecuador", code: "EC" }, + { name: "Egypt", code: "EG" }, + { name: "El Salvador", code: "SV" }, + { name: "Equatorial Guinea", code: "GQ" }, + { name: "Eritrea", code: "ER" }, + { name: "Estonia", code: "EE" }, + { name: "Ethiopia", code: "ET" }, + { name: "Falkland Islands (Malvinas)", code: "FK" }, + { name: "Faroe Islands", code: "FO" }, + { name: "Fiji", code: "FJ" }, + { name: "Finland", code: "FI" }, + { name: "France", code: "FR" }, + { name: "French Guiana", code: "GF" }, + { name: "French Polynesia", code: "PF" }, + { name: "French Southern Territories", code: "TF" }, + { name: "Gabon", code: "GA" }, + { name: "Gambia", code: "GM" }, + { name: "Georgia", code: "GE" }, + { name: "Germany", code: "DE" }, + { name: "Ghana", code: "GH" }, + { name: "Gibraltar", code: "GI" }, + { name: "Greece", code: "GR" }, + { name: "Greenland", code: "GL" }, + { name: "Grenada", code: "GD" }, + { name: "Guadeloupe", code: "GP" }, + { name: "Guam", code: "GU" }, + { name: "Guatemala", code: "GT" }, + { name: "Guernsey", code: "GG" }, + { name: "Guinea", code: "GN" }, + { name: "Guinea-Bissau", code: "GW" }, + { name: "Guyana", code: "GY" }, + { name: "Haiti", code: "HT" }, + { name: "Heard Island and Mcdonald Islands", code: "HM" }, + { name: "Holy See (Vatican City State)", code: "VA" }, + { name: "Honduras", code: "HN" }, + { name: "Hong Kong", code: "HK" }, + { name: "Hungary", code: "HU" }, + { name: "Iceland", code: "IS" }, + { name: "India", code: "IN" }, + { name: "Indonesia", code: "ID" }, + { name: "Iran, Islamic Republic Of", code: "IR" }, + { name: "Iraq", code: "IQ" }, + { name: "Ireland", code: "IE" }, + { name: "Isle of Man", code: "IM" }, + { name: "Israel", code: "IL" }, + { name: "Italy", code: "IT" }, + { name: "Jamaica", code: "JM" }, + { name: "Japan", code: "JP" }, + { name: "Jersey", code: "JE" }, + { name: "Jordan", code: "JO" }, + { name: "Kazakhstan", code: "KZ" }, + { name: "Kenya", code: "KE" }, + { name: "Kiribati", code: "KI" }, + { name: "Korea, Democratic People'S Republic of", code: "KP" }, + { name: "Korea, Republic of", code: "KR" }, + { name: "Kuwait", code: "KW" }, + { name: "Kyrgyzstan", code: "KG" }, + { name: "Lao People'S Democratic Republic", code: "LA" }, + { name: "Latvia", code: "LV" }, + { name: "Lebanon", code: "LB" }, + { name: "Lesotho", code: "LS" }, + { name: "Liberia", code: "LR" }, + { name: "Libyan Arab Jamahiriya", code: "LY" }, + { name: "Liechtenstein", code: "LI" }, + { name: "Lithuania", code: "LT" }, + { name: "Luxembourg", code: "LU" }, + { name: "Macao", code: "MO" }, + { name: "Macedonia, The Former Yugoslav Republic of", code: "MK" }, + { name: "Madagascar", code: "MG" }, + { name: "Malawi", code: "MW" }, + { name: "Malaysia", code: "MY" }, + { name: "Maldives", code: "MV" }, + { name: "Mali", code: "ML" }, + { name: "Malta", code: "MT" }, + { name: "Marshall Islands", code: "MH" }, + { name: "Martinique", code: "MQ" }, + { name: "Mauritania", code: "MR" }, + { name: "Mauritius", code: "MU" }, + { name: "Mayotte", code: "YT" }, + { name: "Mexico", code: "MX" }, + { name: "Micronesia, Federated States of", code: "FM" }, + { name: "Moldova, Republic of", code: "MD" }, + { name: "Monaco", code: "MC" }, + { name: "Mongolia", code: "MN" }, + { name: "Montenegro", code: "ME" }, + { name: "Montserrat", code: "MS" }, + { name: "Morocco", code: "MA" }, + { name: "Mozambique", code: "MZ" }, + { name: "Myanmar", code: "MM" }, + { name: "Namibia", code: "NA" }, + { name: "Nauru", code: "NR" }, + { name: "Nepal", code: "NP" }, + { name: "Netherlands", code: "NL" }, + { name: "Netherlands Antilles", code: "AN" }, + { name: "New Caledonia", code: "NC" }, + { name: "New Zealand", code: "NZ" }, + { name: "Nicaragua", code: "NI" }, + { name: "Niger", code: "NE" }, + { name: "Nigeria", code: "NG" }, + { name: "Niue", code: "NU" }, + { name: "Norfolk Island", code: "NF" }, + { name: "Northern Mariana Islands", code: "MP" }, + { name: "Norway", code: "NO" }, + { name: "Oman", code: "OM" }, + { name: "Pakistan", code: "PK" }, + { name: "Palau", code: "PW" }, + { name: "Palestinian Territory, Occupied", code: "PS" }, + { name: "Panama", code: "PA" }, + { name: "Papua New Guinea", code: "PG" }, + { name: "Paraguay", code: "PY" }, + { name: "Peru", code: "PE" }, + { name: "Philippines", code: "PH" }, + { name: "Pitcairn", code: "PN" }, + { name: "Poland", code: "PL" }, + { name: "Portugal", code: "PT" }, + { name: "Puerto Rico", code: "PR" }, + { name: "Qatar", code: "QA" }, + { name: "Reunion", code: "RE" }, + { name: "Romania", code: "RO" }, + { name: "Russian Federation", code: "RU" }, + { name: "RWANDA", code: "RW" }, + { name: "Saint Helena", code: "SH" }, + { name: "Saint Kitts and Nevis", code: "KN" }, + { name: "Saint Lucia", code: "LC" }, + { name: "Saint Pierre and Miquelon", code: "PM" }, + { name: "Saint Vincent and the Grenadines", code: "VC" }, + { name: "Samoa", code: "WS" }, + { name: "San Marino", code: "SM" }, + { name: "Sao Tome and Principe", code: "ST" }, + { name: "Saudi Arabia", code: "SA" }, + { name: "Senegal", code: "SN" }, + { name: "Serbia", code: "RS" }, + { name: "Seychelles", code: "SC" }, + { name: "Sierra Leone", code: "SL" }, + { name: "Singapore", code: "SG" }, + { name: "Slovakia", code: "SK" }, + { name: "Slovenia", code: "SI" }, + { name: "Solomon Islands", code: "SB" }, + { name: "Somalia", code: "SO" }, + { name: "South Africa", code: "ZA" }, + { name: "South Georgia and the South Sandwich Islands", code: "GS" }, + { name: "Spain", code: "ES" }, + { name: "Sri Lanka", code: "LK" }, + { name: "Sudan", code: "SD" }, + { name: "Suriname", code: "SR" }, + { name: "Svalbard and Jan Mayen", code: "SJ" }, + { name: "Swaziland", code: "SZ" }, + { name: "Sweden", code: "SE" }, + { name: "Switzerland", code: "CH" }, + { name: "Syrian Arab Republic", code: "SY" }, + { name: "Taiwan, Province of China", code: "TW" }, + { name: "Tajikistan", code: "TJ" }, + { name: "Tanzania, United Republic of", code: "TZ" }, + { name: "Thailand", code: "TH" }, + { name: "Timor-Leste", code: "TL" }, + { name: "Togo", code: "TG" }, + { name: "Tokelau", code: "TK" }, + { name: "Tonga", code: "TO" }, + { name: "Trinidad and Tobago", code: "TT" }, + { name: "Tunisia", code: "TN" }, + { name: "Turkey", code: "TR" }, + { name: "Turkmenistan", code: "TM" }, + { name: "Turks and Caicos Islands", code: "TC" }, + { name: "Tuvalu", code: "TV" }, + { name: "Uganda", code: "UG" }, + { name: "Ukraine", code: "UA" }, + { name: "United Arab Emirates", code: "AE" }, + { name: "United Kingdom", code: "GB" }, + { name: "United States", code: "US" }, + { name: "United States Minor Outlying Islands", code: "UM" }, + { name: "Uruguay", code: "UY" }, + { name: "Uzbekistan", code: "UZ" }, + { name: "Vanuatu", code: "VU" }, + { name: "Venezuela", code: "VE" }, + { name: "Viet Nam", code: "VN" }, + { name: "Virgin Islands, British", code: "VG" }, + { name: "Virgin Islands, U.S.", code: "VI" }, + { name: "Wallis and Futuna", code: "WF" }, + { name: "Western Sahara", code: "EH" }, + { name: "Yemen", code: "YE" }, + { name: "Zambia", code: "ZM" }, + { name: "Zimbabwe", code: "ZW" } +]; diff --git a/ems-core/web-admin/src/main.js b/ems-core/web-admin/src/main.js new file mode 100644 index 0000000..89c3963 --- /dev/null +++ b/ems-core/web-admin/src/main.js @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +// Initialize global event bus +import mitt from 'mitt' +export const eventBus = mitt() + +// Initialize Vue App +import { createApp } from 'vue' +import App from './App.vue' +import router from './router.js' + +const app = createApp(App) +app.use(router) +app.mount('#app') diff --git a/ems-core/web-admin/src/resources/.env b/ems-core/web-admin/src/resources/.env new file mode 100644 index 0000000..4db4e8b --- /dev/null +++ b/ems-core/web-admin/src/resources/.env @@ -0,0 +1,2 @@ +VUE_APP_EMS_VERSION=@project.version@ +VUE_APP_EMS_BUILD=@maven.build.timestamp@ \ No newline at end of file diff --git a/ems-core/web-admin/src/router.js b/ems-core/web-admin/src/router.js new file mode 100644 index 0000000..f9c7acc --- /dev/null +++ b/ems-core/web-admin/src/router.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +import { createRouter, createWebHistory } from 'vue-router' +import Admin from './views/admin/admin.vue' +import About from './views/about.vue' +import NotFound from './views/404.vue' + +const routes = [ + { + path: '/', + name: 'Home', + component: Admin + }, + { + path: '/sample', + name: 'Sample', + // route level code-splitting + // this generates a separate chunk (about.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import(/* webpackChunkName: "sample" */ './views/sample/sample.vue') + }, + { + path: '/about', + name: 'About', + component: About + }, + { + path: '/:pathMatch(.*)', + component: NotFound + }, +] + +const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes +}) + +export default router diff --git a/ems-core/web-admin/src/utils.js b/ems-core/web-admin/src/utils.js new file mode 100644 index 0000000..717af1c --- /dev/null +++ b/ems-core/web-admin/src/utils.js @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +const precision = 100; + +class Utils { + valueExists(modelValue, name) { + if (!modelValue) return false; + let part = name.split('.'); + let d = modelValue; + for (let i=0; i=1) { + r++; + d = d / 10; + } + return r; + } + + /*updateSelect(newVal, targetMap, valueField, textField) { + // add new or update targetMap entries + for (let c of newVal) { + if (!targetMap[c[valueField]] || targetMap[c[valueField]].text !== c[textField]) { + targetMap[c[valueField]] = { value: c[valueField], text: c[textField] }; + console.log('updateMap: ADD/UPD: ', c.id, targetMap[c.id]); + } + } + + // remove obsolete targetMap entries + let newVal_ids = newVal.map(o=>o[valueField]); + for (let cid of Object.keys(targetMap)) { + if (!newVal_ids.includes(cid)) { + delete targetMap[cid]; + console.log('updateMap: DEL: ', cid); + } + } + }*/ +} + +var utils = new Utils(); + +export default utils; \ No newline at end of file diff --git a/ems-core/web-admin/src/views/404.vue b/ems-core/web-admin/src/views/404.vue new file mode 100644 index 0000000..79eeb23 --- /dev/null +++ b/ems-core/web-admin/src/views/404.vue @@ -0,0 +1,23 @@ + + diff --git a/ems-core/web-admin/src/views/about.vue b/ems-core/web-admin/src/views/about.vue new file mode 100644 index 0000000..14bdeaf --- /dev/null +++ b/ems-core/web-admin/src/views/about.vue @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/admin-1-overview-header.vue b/ems-core/web-admin/src/views/admin/admin-1-overview-header.vue new file mode 100644 index 0000000..f57fbb3 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin-1-overview-header.vue @@ -0,0 +1,268 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/admin-1-overview.vue b/ems-core/web-admin/src/views/admin/admin-1-overview.vue new file mode 100644 index 0000000..ce9c35a --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin-1-overview.vue @@ -0,0 +1,263 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/admin-1-overview.vue.old b/ems-core/web-admin/src/views/admin/admin-1-overview.vue.old new file mode 100644 index 0000000..1eadf7b --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin-1-overview.vue.old @@ -0,0 +1,263 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/admin-2-topology.vue b/ems-core/web-admin/src/views/admin/admin-2-topology.vue new file mode 100644 index 0000000..1940240 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin-2-topology.vue @@ -0,0 +1,1010 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/admin-3-geography.vue b/ems-core/web-admin/src/views/admin/admin-3-geography.vue new file mode 100644 index 0000000..9846c91 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin-3-geography.vue @@ -0,0 +1,246 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/admin-4-commands.vue b/ems-core/web-admin/src/views/admin/admin-4-commands.vue new file mode 100644 index 0000000..0843eaa --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin-4-commands.vue @@ -0,0 +1,140 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/admin-5-broker-cep.vue b/ems-core/web-admin/src/views/admin/admin-5-broker-cep.vue new file mode 100644 index 0000000..a51153c --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin-5-broker-cep.vue @@ -0,0 +1,23 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/admin.vue b/ems-core/web-admin/src/views/admin/admin.vue new file mode 100644 index 0000000..15ec2b1 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/admin.vue @@ -0,0 +1,112 @@ + + + + + + diff --git a/ems-core/web-admin/src/views/admin/country-coordinates.js b/ems-core/web-admin/src/views/admin/country-coordinates.js new file mode 100644 index 0000000..db4a438 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/country-coordinates.js @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +export default { + 'AD': { code: 'AD', lat: 42.546245, lon: 1.601554, country: 'Andorra' }, + 'AE': { code: 'AE', lat: 23.424076, lon: 53.847818, country: 'United Arab Emirates' }, + 'AF': { code: 'AF', lat: 33.93911, lon: 67.709953, country: 'Afghanistan' }, + 'AG': { code: 'AG', lat: 17.060816, lon: -61.796428, country: 'Antigua and Barbuda' }, + 'AI': { code: 'AI', lat: 18.220554, lon: -63.068615, country: 'Anguilla' }, + 'AL': { code: 'AL', lat: 41.153332, lon: 20.168331, country: 'Albania' }, + 'AM': { code: 'AM', lat: 40.069099, lon: 45.038189, country: 'Armenia' }, + 'AN': { code: 'AN', lat: 12.226079, lon: -69.060087, country: 'Netherlands Antilles' }, + 'AO': { code: 'AO', lat: -11.202692, lon: 17.873887, country: 'Angola' }, + 'AQ': { code: 'AQ', lat: -75.250973, lon: -0.071389, country: 'Antarctica' }, + 'AR': { code: 'AR', lat: -38.416097, lon: -63.616672, country: 'Argentina' }, + 'AS': { code: 'AS', lat: -14.270972, lon: -170.132217, country: 'American Samoa' }, + 'AT': { code: 'AT', lat: 47.516231, lon: 14.550072, country: 'Austria' }, + 'AU': { code: 'AU', lat: -25.274398, lon: 133.775136, country: 'Australia' }, + 'AW': { code: 'AW', lat: 12.52111, lon: -69.968338, country: 'Aruba' }, + 'AZ': { code: 'AZ', lat: 40.143105, lon: 47.576927, country: 'Azerbaijan' }, + 'BA': { code: 'BA', lat: 43.915886, lon: 17.679076, country: 'Bosnia and Herzegovina' }, + 'BB': { code: 'BB', lat: 13.193887, lon: -59.543198, country: 'Barbados' }, + 'BD': { code: 'BD', lat: 23.684994, lon: 90.356331, country: 'Bangladesh' }, + 'BE': { code: 'BE', lat: 50.503887, lon: 4.469936, country: 'Belgium' }, + 'BF': { code: 'BF', lat: 12.238333, lon: -1.561593, country: 'Burkina Faso' }, + 'BG': { code: 'BG', lat: 42.733883, lon: 25.48583, country: 'Bulgaria' }, + 'BH': { code: 'BH', lat: 25.930414, lon: 50.637772, country: 'Bahrain' }, + 'BI': { code: 'BI', lat: -3.373056, lon: 29.918886, country: 'Burundi' }, + 'BJ': { code: 'BJ', lat: 9.30769, lon: 2.315834, country: 'Benin' }, + 'BM': { code: 'BM', lat: 32.321384, lon: -64.75737, country: 'Bermuda' }, + 'BN': { code: 'BN', lat: 4.535277, lon: 114.727669, country: 'Brunei' }, + 'BO': { code: 'BO', lat: -16.290154, lon: -63.588653, country: 'Bolivia' }, + 'BR': { code: 'BR', lat: -14.235004, lon: -51.92528, country: 'Brazil' }, + 'BS': { code: 'BS', lat: 25.03428, lon: -77.39628, country: 'Bahamas' }, + 'BT': { code: 'BT', lat: 27.514162, lon: 90.433601, country: 'Bhutan' }, + 'BV': { code: 'BV', lat: -54.423199, lon: 3.413194, country: 'Bouvet Island' }, + 'BW': { code: 'BW', lat: -22.328474, lon: 24.684866, country: 'Botswana' }, + 'BY': { code: 'BY', lat: 53.709807, lon: 27.953389, country: 'Belarus' }, + 'BZ': { code: 'BZ', lat: 17.189877, lon: -88.49765, country: 'Belize' }, + 'CA': { code: 'CA', lat: 56.130366, lon: -106.346771, country: 'Canada' }, + 'CC': { code: 'CC', lat: -12.164165, lon: 96.870956, country: 'Cocos [Keeling] Islands' }, + 'CD': { code: 'CD', lat: -4.038333, lon: 21.758664, country: 'Congo [DRC]' }, + 'CF': { code: 'CF', lat: 6.611111, lon: 20.939444, country: 'Central African Republic' }, + 'CG': { code: 'CG', lat: -0.228021, lon: 15.827659, country: 'Congo [Republic]' }, + 'CH': { code: 'CH', lat: 46.818188, lon: 8.227512, country: 'Switzerland' }, + 'CI': { code: 'CI', lat: 7.539989, lon: -5.54708, country: 'Côte d\'Ivoire' }, + 'CK': { code: 'CK', lat: -21.236736, lon: -159.777671, country: 'Cook Islands' }, + 'CL': { code: 'CL', lat: -35.675147, lon: -71.542969, country: 'Chile' }, + 'CM': { code: 'CM', lat: 7.369722, lon: 12.354722, country: 'Cameroon' }, + 'CN': { code: 'CN', lat: 35.86166, lon: 104.195397, country: 'China' }, + 'CO': { code: 'CO', lat: 4.570868, lon: -74.297333, country: 'Colombia' }, + 'CR': { code: 'CR', lat: 9.748917, lon: -83.753428, country: 'Costa Rica' }, + 'CU': { code: 'CU', lat: 21.521757, lon: -77.781167, country: 'Cuba' }, + 'CV': { code: 'CV', lat: 16.002082, lon: -24.013197, country: 'Cape Verde' }, + 'CX': { code: 'CX', lat: -10.447525, lon: 105.690449, country: 'Christmas Island' }, + 'CY': { code: 'CY', lat: 35.126413, lon: 33.429859, country: 'Cyprus' }, + 'CZ': { code: 'CZ', lat: 49.817492, lon: 15.472962, country: 'Czech Republic' }, + 'DE': { code: 'DE', lat: 51.165691, lon: 10.451526, country: 'Germany' }, + 'DJ': { code: 'DJ', lat: 11.825138, lon: 42.590275, country: 'Djibouti' }, + 'DK': { code: 'DK', lat: 56.26392, lon: 9.501785, country: 'Denmark' }, + 'DM': { code: 'DM', lat: 15.414999, lon: -61.370976, country: 'Dominica' }, + 'DO': { code: 'DO', lat: 18.735693, lon: -70.162651, country: 'Dominican Republic' }, + 'DZ': { code: 'DZ', lat: 28.033886, lon: 1.659626, country: 'Algeria' }, + 'EC': { code: 'EC', lat: -1.831239, lon: -78.183406, country: 'Ecuador' }, + 'EE': { code: 'EE', lat: 58.595272, lon: 25.013607, country: 'Estonia' }, + 'EG': { code: 'EG', lat: 26.820553, lon: 30.802498, country: 'Egypt' }, + 'EH': { code: 'EH', lat: 24.215527, lon: -12.885834, country: 'Western Sahara' }, + 'ER': { code: 'ER', lat: 15.179384, lon: 39.782334, country: 'Eritrea' }, + 'ES': { code: 'ES', lat: 40.463667, lon: -3.74922, country: 'Spain' }, + 'ET': { code: 'ET', lat: 9.145, lon: 40.489673, country: 'Ethiopia' }, + 'FI': { code: 'FI', lat: 61.92411, lon: 25.748151, country: 'Finland' }, + 'FJ': { code: 'FJ', lat: -16.578193, lon: 179.414413, country: 'Fiji' }, + 'FK': { code: 'FK', lat: -51.796253, lon: -59.523613, country: 'Falkland Islands [Islas Malvinas]' }, + 'FM': { code: 'FM', lat: 7.425554, lon: 150.550812, country: 'Micronesia' }, + 'FO': { code: 'FO', lat: 61.892635, lon: -6.911806, country: 'Faroe Islands' }, + 'FR': { code: 'FR', lat: 46.227638, lon: 2.213749, country: 'France' }, + 'GA': { code: 'GA', lat: -0.803689, lon: 11.609444, country: 'Gabon' }, + 'GB': { code: 'GB', lat: 55.378051, lon: -3.435973, country: 'United Kingdom' }, + 'GD': { code: 'GD', lat: 12.262776, lon: -61.604171, country: 'Grenada' }, + 'GE': { code: 'GE', lat: 42.315407, lon: 43.356892, country: 'Georgia' }, + 'GF': { code: 'GF', lat: 3.933889, lon: -53.125782, country: 'French Guiana' }, + 'GG': { code: 'GG', lat: 49.465691, lon: -2.585278, country: 'Guernsey' }, + 'GH': { code: 'GH', lat: 7.946527, lon: -1.023194, country: 'Ghana' }, + 'GI': { code: 'GI', lat: 36.137741, lon: -5.345374, country: 'Gibraltar' }, + 'GL': { code: 'GL', lat: 71.706936, lon: -42.604303, country: 'Greenland' }, + 'GM': { code: 'GM', lat: 13.443182, lon: -15.310139, country: 'Gambia' }, + 'GN': { code: 'GN', lat: 9.945587, lon: -9.696645, country: 'Guinea' }, + 'GP': { code: 'GP', lat: 16.995971, lon: -62.067641, country: 'Guadeloupe' }, + 'GQ': { code: 'GQ', lat: 1.650801, lon: 10.267895, country: 'Equatorial Guinea' }, + 'GR': { code: 'GR', lat: 39.074208, lon: 21.824312, country: 'Greece' }, + 'GS': { code: 'GS', lat: -54.429579, lon: -36.587909, country: 'South Georgia and the South Sandwich Islands' }, + 'GT': { code: 'GT', lat: 15.783471, lon: -90.230759, country: 'Guatemala' }, + 'GU': { code: 'GU', lat: 13.444304, lon: 144.793731, country: 'Guam' }, + 'GW': { code: 'GW', lat: 11.803749, lon: -15.180413, country: 'Guinea-Bissau' }, + 'GY': { code: 'GY', lat: 4.860416, lon: -58.93018, country: 'Guyana' }, + 'GZ': { code: 'GZ', lat: 31.354676, lon: 34.308825, country: 'Gaza Strip' }, + 'HK': { code: 'HK', lat: 22.396428, lon: 114.109497, country: 'Hong Kong' }, + 'HM': { code: 'HM', lat: -53.08181, lon: 73.504158, country: 'Heard Island and McDonald Islands' }, + 'HN': { code: 'HN', lat: 15.199999, lon: -86.241905, country: 'Honduras' }, + 'HR': { code: 'HR', lat: 45.1, lon: 15.2, country: 'Croatia' }, + 'HT': { code: 'HT', lat: 18.971187, lon: -72.285215, country: 'Haiti' }, + 'HU': { code: 'HU', lat: 47.162494, lon: 19.503304, country: 'Hungary' }, + 'ID': { code: 'ID', lat: -0.789275, lon: 113.921327, country: 'Indonesia' }, + 'IE': { code: 'IE', lat: 53.41291, lon: -8.24389, country: 'Ireland' }, + 'IL': { code: 'IL', lat: 31.046051, lon: 34.851612, country: 'Israel' }, + 'IM': { code: 'IM', lat: 54.236107, lon: -4.548056, country: 'Isle of Man' }, + 'IN': { code: 'IN', lat: 20.593684, lon: 78.96288, country: 'India' }, + 'IO': { code: 'IO', lat: -6.343194, lon: 71.876519, country: 'British Indian Ocean Territory' }, + 'IQ': { code: 'IQ', lat: 33.223191, lon: 43.679291, country: 'Iraq' }, + 'IR': { code: 'IR', lat: 32.427908, lon: 53.688046, country: 'Iran' }, + 'IS': { code: 'IS', lat: 64.963051, lon: -19.020835, country: 'Iceland' }, + 'IT': { code: 'IT', lat: 41.87194, lon: 12.56738, country: 'Italy' }, + 'JE': { code: 'JE', lat: 49.214439, lon: -2.13125, country: 'Jersey' }, + 'JM': { code: 'JM', lat: 18.109581, lon: -77.297508, country: 'Jamaica' }, + 'JO': { code: 'JO', lat: 30.585164, lon: 36.238414, country: 'Jordan' }, + 'JP': { code: 'JP', lat: 36.204824, lon: 138.252924, country: 'Japan' }, + 'KE': { code: 'KE', lat: -0.023559, lon: 37.906193, country: 'Kenya' }, + 'KG': { code: 'KG', lat: 41.20438, lon: 74.766098, country: 'Kyrgyzstan' }, + 'KH': { code: 'KH', lat: 12.565679, lon: 104.990963, country: 'Cambodia' }, + 'KI': { code: 'KI', lat: -3.370417, lon: -168.734039, country: 'Kiribati' }, + 'KM': { code: 'KM', lat: -11.875001, lon: 43.872219, country: 'Comoros' }, + 'KN': { code: 'KN', lat: 17.357822, lon: -62.782998, country: 'Saint Kitts and Nevis' }, + 'KP': { code: 'KP', lat: 40.339852, lon: 127.510093, country: 'North Korea' }, + 'KR': { code: 'KR', lat: 35.907757, lon: 127.766922, country: 'South Korea' }, + 'KW': { code: 'KW', lat: 29.31166, lon: 47.481766, country: 'Kuwait' }, + 'KY': { code: 'KY', lat: 19.513469, lon: -80.566956, country: 'Cayman Islands' }, + 'KZ': { code: 'KZ', lat: 48.019573, lon: 66.923684, country: 'Kazakhstan' }, + 'LA': { code: 'LA', lat: 19.85627, lon: 102.495496, country: 'Laos' }, + 'LB': { code: 'LB', lat: 33.854721, lon: 35.862285, country: 'Lebanon' }, + 'LC': { code: 'LC', lat: 13.909444, lon: -60.978893, country: 'Saint Lucia' }, + 'LI': { code: 'LI', lat: 47.166, lon: 9.555373, country: 'Liechtenstein' }, + 'LK': { code: 'LK', lat: 7.873054, lon: 80.771797, country: 'Sri Lanka' }, + 'LR': { code: 'LR', lat: 6.428055, lon: -9.429499, country: 'Liberia' }, + 'LS': { code: 'LS', lat: -29.609988, lon: 28.233608, country: 'Lesotho' }, + 'LT': { code: 'LT', lat: 55.169438, lon: 23.881275, country: 'Lithuania' }, + 'LU': { code: 'LU', lat: 49.815273, lon: 6.129583, country: 'Luxembourg' }, + 'LV': { code: 'LV', lat: 56.879635, lon: 24.603189, country: 'Latvia' }, + 'LY': { code: 'LY', lat: 26.3351, lon: 17.228331, country: 'Libya' }, + 'MA': { code: 'MA', lat: 31.791702, lon: -7.09262, country: 'Morocco' }, + 'MC': { code: 'MC', lat: 43.750298, lon: 7.412841, country: 'Monaco' }, + 'MD': { code: 'MD', lat: 47.411631, lon: 28.369885, country: 'Moldova' }, + 'ME': { code: 'ME', lat: 42.708678, lon: 19.37439, country: 'Montenegro' }, + 'MG': { code: 'MG', lat: -18.766947, lon: 46.869107, country: 'Madagascar' }, + 'MH': { code: 'MH', lat: 7.131474, lon: 171.184478, country: 'Marshall Islands' }, + 'MK': { code: 'MK', lat: 41.608635, lon: 21.745275, country: 'Macedonia [FYROM]' }, + 'ML': { code: 'ML', lat: 17.570692, lon: -3.996166, country: 'Mali' }, + 'MM': { code: 'MM', lat: 21.913965, lon: 95.956223, country: 'Myanmar [Burma]' }, + 'MN': { code: 'MN', lat: 46.862496, lon: 103.846656, country: 'Mongolia' }, + 'MO': { code: 'MO', lat: 22.198745, lon: 113.543873, country: 'Macau' }, + 'MP': { code: 'MP', lat: 17.33083, lon: 145.38469, country: 'Northern Mariana Islands' }, + 'MQ': { code: 'MQ', lat: 14.641528, lon: -61.024174, country: 'Martinique' }, + 'MR': { code: 'MR', lat: 21.00789, lon: -10.940835, country: 'Mauritania' }, + 'MS': { code: 'MS', lat: 16.742498, lon: -62.187366, country: 'Montserrat' }, + 'MT': { code: 'MT', lat: 35.937496, lon: 14.375416, country: 'Malta' }, + 'MU': { code: 'MU', lat: -20.348404, lon: 57.552152, country: 'Mauritius' }, + 'MV': { code: 'MV', lat: 3.202778, lon: 73.22068, country: 'Maldives' }, + 'MW': { code: 'MW', lat: -13.254308, lon: 34.301525, country: 'Malawi' }, + 'MX': { code: 'MX', lat: 23.634501, lon: -102.552784, country: 'Mexico' }, + 'MY': { code: 'MY', lat: 4.210484, lon: 101.975766, country: 'Malaysia' }, + 'MZ': { code: 'MZ', lat: -18.665695, lon: 35.529562, country: 'Mozambique' }, + 'NA': { code: 'NA', lat: -22.95764, lon: 18.49041, country: 'Namibia' }, + 'NC': { code: 'NC', lat: -20.904305, lon: 165.618042, country: 'New Caledonia' }, + 'NE': { code: 'NE', lat: 17.607789, lon: 8.081666, country: 'Niger' }, + 'NF': { code: 'NF', lat: -29.040835, lon: 167.954712, country: 'Norfolk Island' }, + 'NG': { code: 'NG', lat: 9.081999, lon: 8.675277, country: 'Nigeria' }, + 'NI': { code: 'NI', lat: 12.865416, lon: -85.207229, country: 'Nicaragua' }, + 'NL': { code: 'NL', lat: 52.132633, lon: 5.291266, country: 'Netherlands' }, + 'NO': { code: 'NO', lat: 60.472024, lon: 8.468946, country: 'Norway' }, + 'NP': { code: 'NP', lat: 28.394857, lon: 84.124008, country: 'Nepal' }, + 'NR': { code: 'NR', lat: -0.522778, lon: 166.931503, country: 'Nauru' }, + 'NU': { code: 'NU', lat: -19.054445, lon: -169.867233, country: 'Niue' }, + 'NZ': { code: 'NZ', lat: -40.900557, lon: 174.885971, country: 'New Zealand' }, + 'OM': { code: 'OM', lat: 21.512583, lon: 55.923255, country: 'Oman' }, + 'PA': { code: 'PA', lat: 8.537981, lon: -80.782127, country: 'Panama' }, + 'PE': { code: 'PE', lat: -9.189967, lon: -75.015152, country: 'Peru' }, + 'PF': { code: 'PF', lat: -17.679742, lon: -149.406843, country: 'French Polynesia' }, + 'PG': { code: 'PG', lat: -6.314993, lon: 143.95555, country: 'Papua New Guinea' }, + 'PH': { code: 'PH', lat: 12.879721, lon: 121.774017, country: 'Philippines' }, + 'PK': { code: 'PK', lat: 30.375321, lon: 69.345116, country: 'Pakistan' }, + 'PL': { code: 'PL', lat: 51.919438, lon: 19.145136, country: 'Poland' }, + 'PM': { code: 'PM', lat: 46.941936, lon: -56.27111, country: 'Saint Pierre and Miquelon' }, + 'PN': { code: 'PN', lat: -24.703615, lon: -127.439308, country: 'Pitcairn Islands' }, + 'PR': { code: 'PR', lat: 18.220833, lon: -66.590149, country: 'Puerto Rico' }, + 'PS': { code: 'PS', lat: 31.952162, lon: 35.233154, country: 'Palestinian Territories' }, + 'PT': { code: 'PT', lat: 39.399872, lon: -8.224454, country: 'Portugal' }, + 'PW': { code: 'PW', lat: 7.51498, lon: 134.58252, country: 'Palau' }, + 'PY': { code: 'PY', lat: -23.442503, lon: -58.443832, country: 'Paraguay' }, + 'QA': { code: 'QA', lat: 25.354826, lon: 51.183884, country: 'Qatar' }, + 'RE': { code: 'RE', lat: -21.115141, lon: 55.536384, country: 'Réunion' }, + 'RO': { code: 'RO', lat: 45.943161, lon: 24.96676, country: 'Romania' }, + 'RS': { code: 'RS', lat: 44.016521, lon: 21.005859, country: 'Serbia' }, + 'RU': { code: 'RU', lat: 61.52401, lon: 105.318756, country: 'Russia' }, + 'RW': { code: 'RW', lat: -1.940278, lon: 29.873888, country: 'Rwanda' }, + 'SA': { code: 'SA', lat: 23.885942, lon: 45.079162, country: 'Saudi Arabia' }, + 'SB': { code: 'SB', lat: -9.64571, lon: 160.156194, country: 'Solomon Islands' }, + 'SC': { code: 'SC', lat: -4.679574, lon: 55.491977, country: 'Seychelles' }, + 'SD': { code: 'SD', lat: 12.862807, lon: 30.217636, country: 'Sudan' }, + 'SE': { code: 'SE', lat: 60.128161, lon: 18.643501, country: 'Sweden' }, + 'SG': { code: 'SG', lat: 1.352083, lon: 103.819836, country: 'Singapore' }, + 'SH': { code: 'SH', lat: -24.143474, lon: -10.030696, country: 'Saint Helena' }, + 'SI': { code: 'SI', lat: 46.151241, lon: 14.995463, country: 'Slovenia' }, + 'SJ': { code: 'SJ', lat: 77.553604, lon: 23.670272, country: 'Svalbard and Jan Mayen' }, + 'SK': { code: 'SK', lat: 48.669026, lon: 19.699024, country: 'Slovakia' }, + 'SL': { code: 'SL', lat: 8.460555, lon: -11.779889, country: 'Sierra Leone' }, + 'SM': { code: 'SM', lat: 43.94236, lon: 12.457777, country: 'San Marino' }, + 'SN': { code: 'SN', lat: 14.497401, lon: -14.452362, country: 'Senegal' }, + 'SO': { code: 'SO', lat: 5.152149, lon: 46.199616, country: 'Somalia' }, + 'SR': { code: 'SR', lat: 3.919305, lon: -56.027783, country: 'Suriname' }, + 'ST': { code: 'ST', lat: 0.18636, lon: 6.613081, country: 'São Tomé and Príncipe' }, + 'SV': { code: 'SV', lat: 13.794185, lon: -88.89653, country: 'El Salvador' }, + 'SY': { code: 'SY', lat: 34.802075, lon: 38.996815, country: 'Syria' }, + 'SZ': { code: 'SZ', lat: -26.522503, lon: 31.465866, country: 'Swaziland' }, + 'TC': { code: 'TC', lat: 21.694025, lon: -71.797928, country: 'Turks and Caicos Islands' }, + 'TD': { code: 'TD', lat: 15.454166, lon: 18.732207, country: 'Chad' }, + 'TF': { code: 'TF', lat: -49.280366, lon: 69.348557, country: 'French Southern Territories' }, + 'TG': { code: 'TG', lat: 8.619543, lon: 0.824782, country: 'Togo' }, + 'TH': { code: 'TH', lat: 15.870032, lon: 100.992541, country: 'Thailand' }, + 'TJ': { code: 'TJ', lat: 38.861034, lon: 71.276093, country: 'Tajikistan' }, + 'TK': { code: 'TK', lat: -8.967363, lon: -171.855881, country: 'Tokelau' }, + 'TL': { code: 'TL', lat: -8.874217, lon: 125.727539, country: 'Timor-Leste' }, + 'TM': { code: 'TM', lat: 38.969719, lon: 59.556278, country: 'Turkmenistan' }, + 'TN': { code: 'TN', lat: 33.886917, lon: 9.537499, country: 'Tunisia' }, + 'TO': { code: 'TO', lat: -21.178986, lon: -175.198242, country: 'Tonga' }, + 'TR': { code: 'TR', lat: 38.963745, lon: 35.243322, country: 'Turkey' }, + 'TT': { code: 'TT', lat: 10.691803, lon: -61.222503, country: 'Trinidad and Tobago' }, + 'TV': { code: 'TV', lat: -7.109535, lon: 177.64933, country: 'Tuvalu' }, + 'TW': { code: 'TW', lat: 23.69781, lon: 120.960515, country: 'Taiwan' }, + 'TZ': { code: 'TZ', lat: -6.369028, lon: 34.888822, country: 'Tanzania' }, + 'UA': { code: 'UA', lat: 48.379433, lon: 31.16558, country: 'Ukraine' }, + 'UG': { code: 'UG', lat: 1.373333, lon: 32.290275, country: 'Uganda' }, + 'UM': { code: 'UM', lat: 0, lon: 0, country: 'U.S. Minor Outlying Islands' }, + 'US': { code: 'US', lat: 37.09024, lon: -95.712891, country: 'United States' }, + 'UY': { code: 'UY', lat: -32.522779, lon: -55.765835, country: 'Uruguay' }, + 'UZ': { code: 'UZ', lat: 41.377491, lon: 64.585262, country: 'Uzbekistan' }, + 'VA': { code: 'VA', lat: 41.902916, lon: 12.453389, country: 'Vatican City' }, + 'VC': { code: 'VC', lat: 12.984305, lon: -61.287228, country: 'Saint Vincent and the Grenadines' }, + 'VE': { code: 'VE', lat: 6.42375, lon: -66.58973, country: 'Venezuela' }, + 'VG': { code: 'VG', lat: 18.420695, lon: -64.639968, country: 'British Virgin Islands' }, + 'VI': { code: 'VI', lat: 18.335765, lon: -64.896335, country: 'U.S. Virgin Islands' }, + 'VN': { code: 'VN', lat: 14.058324, lon: 108.277199, country: 'Vietnam' }, + 'VU': { code: 'VU', lat: -15.376706, lon: 166.959158, country: 'Vanuatu' }, + 'WF': { code: 'WF', lat: -13.768752, lon: -177.156097, country: 'Wallis and Futuna' }, + 'WS': { code: 'WS', lat: -13.759029, lon: -172.104629, country: 'Samoa' }, + 'XK': { code: 'XK', lat: 42.602636, lon: 20.902977, country: 'Kosovo' }, + 'YE': { code: 'YE', lat: 15.552727, lon: 48.516388, country: 'Yemen' }, + 'YT': { code: 'YT', lat: -12.8275, lon: 45.166244, country: 'Mayotte' }, + 'ZA': { code: 'ZA', lat: -30.559482, lon: 22.937506, country: 'South Africa' }, + 'ZM': { code: 'ZM', lat: -13.133897, lon: 27.849332, country: 'Zambia' }, + 'ZW': { code: 'ZW', lat: -19.015438, lon: 29.154857, country: 'Zimbabwe' }, +}; \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/old/admin.html b/ems-core/web-admin/src/views/admin/old/admin.html new file mode 100644 index 0000000..7d20369 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/old/admin.html @@ -0,0 +1,609 @@ + + +
    +
    + + +
    +
    + + {{ems['_ems-system-info-jvm-uptime']}} + +
    + Loading... +
    + +
    +  hh:mm:ss + + +
    +
    + +
    + {{ems['_ems-system-info-jvm-memory-free'] ?? '-'}} MB + + +
    + +
    + {{ems['_ems-system-info-jvm-memory-max'] ?? '-'}} MB + + +
    + +
    + {{ems['_ems-system-info-jvm-memory-total'] ?? '-'}} MB + + +
    + +
    + + + + +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + + + + + + +
    + +
    + + + + +
    +
    + Some quick example text to build on the card title and make up the bulk of the card's + content. + +
    + + +
    + Some quick example text to build on the card title and make up the bulk of the card's + content. + +
    + + +
    + + + With supporting text below as a natural lead-in to additional content. + + +
    + + +
    + + + Start creating your amazing application! + +
    + +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IdClientAddressLocationStatusStatistics
    {{c.id}}{{c.name}}{{c.address}} + {{c.country??'-'}}
    + Lat: {{c.lat??'-'}}
    + Lon: {{c.lon??'-'}} +
    {{c.status??'Unknown'}} +
    +
    + CPU: + + + +
    +
    + Mem: + + + +
    +
    + #Events:
    + {{c.stats.events??'--'}} +
    +
    + Uptime:
    + {{c.stats.uptime ? toIsoFormat(c.stats.uptime,'sec','time') : '--:--:--'}} +
    +
    +
    + + +
    + +
    + +
    + + + +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IdClientAddressLocationStatusStatistics
    {{c.id}}{{c.name}}{{c.address}} + {{c.country??'-'}}
    + Lat: {{c.lat??'-'}}
    + Lon: {{c.lon??'-'}} +
    {{c.status??'Unknown'}} +
    +
    + CPU: + +
    +
    + Mem: + +
    +
    + #Events:
    + +
    +
    + Uptime:
    + {{c.stats.uptime ? toIsoFormat(c.stats.uptime,'sec','time') : '--:--:--'}} +
    +
    +
    + + +
    + +
    + +
    + +
    + +     + +     + +     + +     + +     + +     +
    +
    +
    + +
    + +
    +
    + + + +
    + +
    + + + +
    + +
    + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    + diff --git a/ems-core/web-admin/src/views/admin/old/admin.js b/ems-core/web-admin/src/views/admin/old/admin.js new file mode 100644 index 0000000..d369de5 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/old/admin.js @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +/* + vue-world-map: + https://www.npmjs.com/package/vue-world-map + https://github.com/Ghrehh/vue-world-map + jquery-ui (for 'sortable' and 'resizable' widgets) + https://www.npmjs.com/package/jquery-ui + https://jqueryui.com/ + Geolocation: + https://stackoverflow.com/questions/391979/how-to-get-clients-ip-address-using-javascript + https://stackoverflow.com/questions/4937517/ip-to-location-using-javascript + https://ipapi.co/ + */ + +import SmallBox from '@/components/smallbox/smallbox.vue' +import InfoBox from '@/components/infobox/infobox.vue' +import Card from '@/components/card/card.vue' + +import VueGauge from 'vue-gauge'; // See https://www.npmjs.com/package/vue-gauge +import ChartJs from '@/components/chartjs/chartjs.vue'; +import WorldMap from '@/components/worldmap/WorldMap.vue'; +import Knob from '@/components/knob/knob.vue'; +import Sparkline from '@/components/sparkline/sparkline.vue'; +import JVectorMap from '@/components/jvectormap/jvectormap.vue'; +//import JQVMap from '@/components/jqvmap/jqvmap.vue'; // Not working; use jvectormap + +import StatusLed from '@/components/status-led/status-led.vue'; + +var $ = require('jquery'); +require('jquery-ui/ui/widgets/resizable.js'); +require('jquery-ui/ui/widgets/sortable.js'); +require('jquery-ui/themes/base/resizable.css'); +require('jquery-ui/themes/base/sortable.css'); + +const EMS_DATA_PREFIX = '_ems'; + +export default { + name: 'Admin Dashboard', + props: { + /*version: String*/ + }, + components: { + SmallBox, InfoBox, Card, + VueGauge, ChartJs, WorldMap, + Knob, Sparkline, JVectorMap, //JQVMap, + StatusLed, + }, + mounted() { + // Make widgets sortable (i.e. movable) + $('section.content div.row').sortable({ + placeholder: 'sort-highlight', + connectWith: 'section.content div.row', + handle: '.card-header, .nav-tabs', + forcePlaceholderSize: true, + zIndex: 999999 + }); + $('.connectedSortable .card-header').css('cursor', 'move'); + + // Make card widgets resizable (not working well with maps) + $('.card').resizable({ + handles: 'all', + }); + }, + + data() { + return { + ems: { }, + clientsData: {}, + clients: [ + {id:'#00000', name:'vm-0000-xxxxx-323', address:'147.102.17.76', loc:'US-west', status:'Up', stats:{cpu:.85,mem:.38,events:345,uptime:12421432}}, + {id:'#00001', name:'vm-1111-yyyyy-323', address:'8.8.8.8', loc:null, status:'Down', stats:'todo'}, + {id:'#00002', name:'vm-2222-zzzzz-323', address:'79.166.188.138', loc:'Germany', stats:'todo'}, + ], + geolocationCache: { + //"147.102.17.76": { + "172.18.0.3": { + //"ip": "147.102.17.76", + "ip": "172.18.0.3", + "version": "IPv4", + "city": "Athens", + "region": "Attica", + "region_code": "I", + "country": "GR", + "country_name": "Greece", + "country_code": "GR", + "country_code_iso3": "GRC", + "country_capital": "Athens", + "country_tld": ".gr", + "continent_code": "EU", + "in_eu": true, + "postal": null, + "latitude": 37.9842, + "longitude": 23.7353, + "timezone": "Europe/Athens", + "utc_offset": "+0300", + "country_calling_code": "+30", + "currency": "EUR", + "currency_name": "Euro", + "languages": "el-GR,en,fr", + "country_area": 131940, + "country_population": 10727668, + "asn": "AS3323", + "org": "National Technical University of Athens" + }, + "8.8.8.8": { + "ip": "8.8.8.8", + "version": "IPv4", + "city": "Mountain View", + "region": "California", + "region_code": "CA", + "country": "US", + "country_name": "United States", + "country_code": "US", + "country_code_iso3": "USA", + "country_capital": "Washington", + "country_tld": ".us", + "continent_code": "NA", + "in_eu": false, + "postal": "Sign up to access", + "latitude": "Sign up to access", + "longitude": "Sign up to access", + "timezone": "America/Los_Angeles", + "utc_offset": "-0700", + "country_calling_code": "+1", + "currency": "USD", + "currency_name": "Dollar", + "languages": "en-US,es-US,haw,fr", + "country_area": 9629091, + "country_population": 327167434, + "message": "Please message us at ipapi.co/trial for full access", + "asn": "AS15169", + "org": "GOOGLE" + }, + "79.166.188.138": { + "ip": "79.166.188.138", + "version": "IPv4", + "city": "Athens", + "region": "Attica", + "region_code": "I", + "country": "GR", + "country_name": "Greece", + "country_code": "GR", + "country_code_iso3": "GRC", + "country_capital": "Athens", + "country_tld": ".gr", + "continent_code": "EU", + "in_eu": true, + "postal": null, + "latitude": 37.9842, + "longitude": 23.7353, + "timezone": "Europe/Athens", + "utc_offset": "+0300", + "country_calling_code": "+30", + "currency": "EUR", + "currency_name": "Euro", + "languages": "el-GR,en,fr", + "country_area": 131940, + "country_population": 10727668, + "asn": "AS3329", + "org": "Vodafone-panafon Hellenic Telecommunications Company SA" + } + }, + clientsPerCountry: { + US: 1, + CA: 7, + GB: 14, + }, + knobs: { + k1:30, k2:70, k3:-80, k4:40, k5:90, k6:50, + k7:30, k8:30, k9:30, k10:30, + k11:80, k12:60, k13:10, k14:100, + } + }; + }, + + + watch: { + ems: function(newVal) { + // Flatten EMS server data + let _flattened_data = {}; + this.flattenData(newVal, EMS_DATA_PREFIX, _flattened_data); + //console.log('Dashboard: FLATTENED_DATA: ', _flattened_data); + Object.assign(newVal, _flattened_data); + + // Convert bytes to MB and timestamps to W3C format + this.prepareData(newVal); + //console.log('Dashboard: FINAL_DATA: ', newVal); + }, + clientsData: function(newVal) { + console.log('>>>>> clientsData: newVal: ', newVal); + + this.clients = []; + for (let k in newVal['client-metrics']) { + if (k.startsWith('#')) { + let v = newVal['client-metrics'][k]; + //console.log(k, '=>', v); + let info = v['client-info']; + let c = { + id: k, + name: '-', + address: info['ip-address'], + loc: '-', + status: 'Up', + stats: { cpu:.85, mem:.38, events:v['count-total-events'], uptime:12421432} + }; + this.clients.push(c); + } + } + + this.addGeolocationInfo(this.clients, this.geolocationCache); + } + }, + + methods: { + flattenData(data, prefix, _flattened_data) { + prefix = prefix.trim(); + if (typeof(data)==='object') { + for (const [key, value] of Object.entries(data)) { + var new_prefix = (prefix!=='') + ? prefix+'-'+key.replace('_','-') + : key.replace('_','-'); + this.flattenData(value, new_prefix, _flattened_data); + } + } else { + _flattened_data[prefix] = ''+data; + } + }, + prepareData(data) { + this.toIsoFormat2(data, '_ems-system-info-jvm-uptime', 'sec', 'time'); + this.toMB(data, '_ems-system-info-jvm-memory-free'); + this.toMB(data, '_ems-system-info-jvm-memory-max'); + this.toMB(data, '_ems-system-info-jvm-memory-total'); + }, + toIsoFormat(data, inUnit, outPart) { + let mult = inUnit==='s' || inUnit==='sec' ? 1000 : 1; + let start = 0; + let len = 100; + if (outPart==='time') { start = 11; len = 8; } + if (outPart==='time+frac') { start = 11; } + if (outPart==='frac' || outPart==='fraction') { start = 19; len = 4; } + if (outPart==='date') { start = 0; len = 10; } + if (outPart==='datetime') { start = 0; len = 19; } + if (outPart==='tz' || outPart==='timezone') { start = 23; len = 1; } + return new Date(data * mult).toISOString().substr(start, len); + }, + toIsoFormat2(data, _key, inUnit, outPart) { + if (data[_key]) + data[_key] = this.toIsoFormat(data[_key], inUnit, outPart); + //console.log(data[_key]); + }, + toMB(data, _key) { + if (data[_key]) + data[_key] = Math.round(data[_key] / 1024 / 1024).toString(); + }, + addGeolocationInfo(dataArray, cache) { + dataArray.forEach(c => { + this.updateGeolocationInfoByIpAddress(c.address, c, cache); + }); + }, + updateGeolocationInfoByIpAddress(ipAddress, obj, cache) { + // get cached info (if available) + if (cache[ipAddress]) { + this.updateGeolocationInfo(obj, cache[ipAddress]); + return; + } + + // call geolocation service + $.getJSON("https://ipapi.co/"+ipAddress+"/json") + .done(function(json) { + console.log('AJAX: done: ', json); + // cache geolocation info + cache[ipAddress] = json; + + this.updateGeolocationInfo(obj, json); + }) + .fail(function(jqxhr, textStatus, error) { + console.log('AJAX: fail: ', jqxhr, textStatus, error); + }) + .always(function(/*jqxhr, textStatus*/) { + //console.log('AJAX: always: ', jqxhr, textStatus); + }); + }, + updateGeolocationInfo(obj, json) { + // check if changed + let emitEvent = true; + /*if (obj.lat && obj.lon && obj.country) { + emitEvent = ! (obj.lat==json.latitude && obj.lon==json.longitude && obj.country==json.country_name); + }*/ + + obj.geo = json; + obj.lat = json.latitude; + obj.lon = json.longitude; + obj.country = json.country_name; + + if (emitEvent) { + //XXX: TODO: Add emit update event in 'updateGeolocationInfoByIpAddress' + //this.$emit('update:clientsValue', dataArray); + } + }, + }, +} diff --git a/ems-core/web-admin/src/views/admin/old/admin.vue b/ems-core/web-admin/src/views/admin/old/admin.vue new file mode 100644 index 0000000..b3532b5 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/old/admin.vue @@ -0,0 +1,11 @@ + + + + diff --git a/ems-core/web-admin/src/views/admin/widgets/cdo-mgnt.vue b/ems-core/web-admin/src/views/admin/widgets/cdo-mgnt.vue new file mode 100644 index 0000000..fb3fa65 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/cdo-mgnt.vue @@ -0,0 +1,456 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/client-commands.vue b/ems-core/web-admin/src/views/admin/widgets/client-commands.vue new file mode 100644 index 0000000..700422b --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/client-commands.vue @@ -0,0 +1,157 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/client-events.vue b/ems-core/web-admin/src/views/admin/widgets/client-events.vue new file mode 100644 index 0000000..d30124c --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/client-events.vue @@ -0,0 +1,392 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/clients-list.vue b/ems-core/web-admin/src/views/admin/widgets/clients-list.vue new file mode 100644 index 0000000..8342378 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/clients-list.vue @@ -0,0 +1,44 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/command-input.vue b/ems-core/web-admin/src/views/admin/widgets/command-input.vue new file mode 100644 index 0000000..fb01274 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/command-input.vue @@ -0,0 +1,38 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/destinations-list.vue b/ems-core/web-admin/src/views/admin/widgets/destinations-list.vue new file mode 100644 index 0000000..34bbfcb --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/destinations-list.vue @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/fileexplorer.vue b/ems-core/web-admin/src/views/admin/widgets/fileexplorer.vue new file mode 100644 index 0000000..e21dcdf --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/fileexplorer.vue @@ -0,0 +1,398 @@ + + + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/img/blank-64.png b/ems-core/web-admin/src/views/admin/widgets/img/blank-64.png new file mode 100644 index 0000000..ee12542 Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/blank-64.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/img/camel-model-32.png b/ems-core/web-admin/src/views/admin/widgets/img/camel-model-32.png new file mode 100644 index 0000000..48c790f Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/camel-model-32.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/img/camel-model-64.png b/ems-core/web-admin/src/views/admin/widgets/img/camel-model-64.png new file mode 100644 index 0000000..01c0857 Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/camel-model-64.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/img/camel-model-80.png b/ems-core/web-admin/src/views/admin/widgets/img/camel-model-80.png new file mode 100644 index 0000000..ca34f65 Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/camel-model-80.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/img/cp-model-32.png b/ems-core/web-admin/src/views/admin/widgets/img/cp-model-32.png new file mode 100644 index 0000000..7e25d6f Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/cp-model-32.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/img/cp-model-64.png b/ems-core/web-admin/src/views/admin/widgets/img/cp-model-64.png new file mode 100644 index 0000000..b738d8e Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/cp-model-64.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/img/folder-64.png b/ems-core/web-admin/src/views/admin/widgets/img/folder-64.png new file mode 100644 index 0000000..ed4a963 Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/folder-64.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/img/unknown-64.png b/ems-core/web-admin/src/views/admin/widgets/img/unknown-64.png new file mode 100644 index 0000000..69816d2 Binary files /dev/null and b/ems-core/web-admin/src/views/admin/widgets/img/unknown-64.png differ diff --git a/ems-core/web-admin/src/views/admin/widgets/latest-events.vue b/ems-core/web-admin/src/views/admin/widgets/latest-events.vue new file mode 100644 index 0000000..4fc3e33 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/latest-events.vue @@ -0,0 +1,130 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/node-actions-list.vue b/ems-core/web-admin/src/views/admin/widgets/node-actions-list.vue new file mode 100644 index 0000000..a5fb9e9 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/node-actions-list.vue @@ -0,0 +1,60 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/rest-call-forms.js b/ems-core/web-admin/src/views/admin/widgets/rest-call-forms.js new file mode 100644 index 0000000..ec138f8 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/rest-call-forms.js @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017-2023 Institute of Communication and Computer Systems (imu.iccs.gr) + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v2.0, unless + * Esper library is used, in which case it is subject to the terms of General Public License v2.0. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * https://www.mozilla.org/en-US/MPL/2.0/ + */ + +export const FORM_TYPE_OPTIONS = [ + { + 'id': 'basic-api-group', + 'text': 'Basic API - Model Translation', + 'priority': -1000, + 'options': [ + { 'id': 'new-app-model', 'text': 'Send App. model request', 'url': '/camelModel', 'method': 'POST', 'form': 'app-model-form', 'priority': 1 }, + { 'id': 'new-cp-model', 'text': 'Send CP model request', 'url': '/cpModelJson', 'method': 'POST', 'form': 'cp-model-form', 'priority': 2 }, + { 'id': 'constants-update', 'text': 'Set constants (add name-value pairs in Payload)', 'url': '/cpConstants', 'method': 'POST', 'form': '', 'priority': 3 }, + { 'id': 'get-app-model', 'text': 'Current App. model', 'url': '/translator/currentAppModel', 'method': 'GET', 'form': '', 'priority': 4 }, + { 'id': 'get-cp-model', 'text': 'Current CP model', 'url': '/translator/currentCpModel', 'method': 'GET', 'form': '', 'priority': 5 }, + ] + }, + + { + 'id': 'topology-group', + 'text': 'Topology', + 'priority': 1, + 'options': [ + { 'id': 'new-vm', 'text': 'Register Node', 'url': '/baguette/registerNode', 'method': 'POST', 'form': 'vm-form', 'priority': 1 }, + { 'id': 'vm-list', 'text': 'Node IP addresses', 'url': '/baguette/node/list', 'method': 'GET', 'form': '', 'priority': 2 }, + { 'id': 'vm-reinstall', 'text': 'Reinstall Node', 'url': '/baguette/node/reinstall/{ip-address}', 'method': 'GET', 'form': 'vm-reinstall', 'priority': 3 }, + + { 'id': 'topology-group-sep1', 'text': '-', 'disabled': true, 'priority': 4 }, + + { 'id': 'client-list', 'text': 'Client list', 'url': '/client/list', 'method': 'GET', 'form': '', 'priority': 11 }, + { 'id': 'client-map', 'text': 'Client map', 'url': '/client/list/map', 'method': 'GET', 'form': '', 'priority': 12 }, + { 'id': 'node-info', 'text': 'Node Info by IP address', 'url': '/baguette/getNodeInfoByAddress/{ip-address}', 'method': 'GET', 'form': 'ip-form', 'priority': 13 }, + { 'id': 'node-name', 'text': 'Node Name by IP address', 'url': '/baguette/getNodeNameByAddress/{ip-address}', 'method': 'GET', 'form': 'ip-form', 'priority': 14 }, + ] + }, + + { + 'id': 'credentials-group', + 'text': 'Credentials', + 'priority': 1001, + 'options': [ + { 'id': 'get-cred', 'text': 'EMS server Broker credentials', 'url': '/broker/credentials', 'method': 'GET', 'form': '', 'priority': 1 }, + { 'id': 'get-ref', 'text': 'VM credentials by Ref', 'url': '/baguette/ref/{ref}', 'method': 'GET', 'form': 'ref-form', 'priority': 2 }, + { 'id': 'new-otp', 'text': 'New OTP', 'url': '/ems/otp/new', 'method': 'GET', 'form': '', 'priority': 3 }, + { 'id': 'del-otp', 'text': 'Delete OTP', 'url': '/ems/otp/remove/{otp}', 'method': 'GET', 'form': 'otp-form', 'priority': 4 }, + ] + }, + + { + 'id': 'observability-group', + 'text': 'Observability', + 'priority': 1002, + 'options': [ + { 'id': 'get-all-logger-levels', 'text': 'Get All Loggers', 'url': '/actuator/loggers', 'method': 'GET', 'form': '', 'priority': 1 }, + { 'id': 'get-logger-level', 'text': 'Get Logger Level', 'url': '/actuator/loggers/{logger}', 'method': 'GET', 'form': 'logger-form', 'priority': 2 }, + { 'id': 'set-logger-level', 'text': 'Set Logger Level', 'url': '/actuator/loggers/{logger}', 'method': 'POST', 'form': 'logger-form', 'priority': 3 }, + + { 'id': 'health', 'text': 'Health check', 'url': '/health', 'method': 'GET', 'form': '', 'priority': 4 } + ] + }, + + { + 'id': 'debug-group', + 'text': 'Debug calls', + 'priority': 1003, + 'options': [ + { 'id': 'd-stop-baguette', 'text': 'Stop Baguette Server', 'url': '/baguette/stopServer', 'method': 'GET', 'form': '', 'priority': 1 }, + { 'id': 'd-shutdown', 'text': 'EMS server shutdown', 'url': '/ems/shutdown', 'method': 'GET', 'form': '', 'priority': 2 }, + { 'id': 'd-exit', 'text': 'EMS server shutdown and Exit', 'url': '/ems/exit', 'method': 'GET', 'form': '', 'priority': 3 }, + { 'id': 'd-restart', 'text': 'EMS server shutdown and Restart', 'url': '/ems/exit/99', 'method': 'GET', 'form': '', 'priority': 4 } + ] + } + ]; + +export const FORM_SPECS = { + '': { 'fields': [] }, + 'app-model-form': { + 'fields': [ + { 'name': 'applicationId', 'text': 'App. model path' }, + { 'name': 'notificationURI', 'text': 'Notification URI' }, + { 'name': 'watermark.user', 'text': '-- User', 'defaultValue': function(_this) { return ('authUsername' in _this) ? _this.authUsername : ('username' in _this) ? _this.username : 'unknown'; } }, + { 'name': 'watermark.system', 'text': '-- System', 'defaultValue': 'Ems-Web-Admin' }, + { 'name': 'watermark.uuid', 'text': '-- UUID', 'defaultValue': function(_this) { return _this.create_UUID(); } }, + { 'name': 'watermark.date', 'text': '-- Date', 'defaultValue': function() { return new Date().getTime(); } }, + ], + }, + 'cp-model-form': { + 'fields': [ + { 'name': 'cp-model-id', 'text': 'CP model path' }, + ] + }, + 'vm-form': { + 'fields': [ + { 'name': 'id', 'text': 'VM Id' }, + { 'name': 'name', 'text': 'VM Name' }, + { 'name': 'operatingSystem', 'text': 'VM OS', 'defaultValue': 'UBUNTU' }, + { 'name': 'type', 'text': 'VM type', 'defaultValue': 'VM' }, + { 'name': 'provider', 'text': 'VM provider' }, + { 'name': 'address', 'text': 'IP address' }, + { 'name': 'ssh.port', 'text': 'SSH port', 'defaultValue': '22' }, + { 'name': 'ssh.username', 'text': 'SSH username' }, + { 'name': 'ssh.password', 'text': 'SSH password', 'type': 'password' }, + { 'name': 'ssh.key', 'text': 'SSH key', 'type': 'password' }, + ] + }, + 'vm-reinstall': { + 'fields': [ + { 'name': 'ip-address', 'text': 'IP address' }, + ] + }, + 'logger-form': { + 'fields': [ + { 'name': 'logger', 'text': 'Logger name' }, + { 'name': 'configuredLevel', 'text': 'New Level' }, + ] + }, + 'ref-form': { + 'fields': [ + { 'name': 'ref', 'text': 'VM reference' }, + ] + }, + 'ip-form': { + 'fields': [ + { 'name': 'ip-address', 'text': 'IP Address' }, + ] + }, + 'otp-form': { + 'fields': [ + { 'name': 'otp', 'text': 'OTP' }, + ] + }, + }; diff --git a/ems-core/web-admin/src/views/admin/widgets/rest-call.vue b/ems-core/web-admin/src/views/admin/widgets/rest-call.vue new file mode 100644 index 0000000..79625f7 --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/rest-call.vue @@ -0,0 +1,444 @@ + + + + \ No newline at end of file diff --git a/ems-core/web-admin/src/views/admin/widgets/textarea-dnd.vue b/ems-core/web-admin/src/views/admin/widgets/textarea-dnd.vue new file mode 100644 index 0000000..eda3b4b --- /dev/null +++ b/ems-core/web-admin/src/views/admin/widgets/textarea-dnd.vue @@ -0,0 +1,61 @@ + +