diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d449d82e7c9e46b43cb0564fd5fc2c7b9f4e4ac4..f62e9dc9b7afd331767b7e264d9bcc3dd227a3c2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ variables: DOCKER_PUSH: "false" LOCAL_REPO: "127.0.0.1:5000" DOCKER_REPO: "gitlab.ow2.org:4567" - MAVEN_IMAGE: "maven:3.5.2-jdk-8" + MAVEN_IMAGE: "maven:3.6.3-jdk-8" DOCKER_DIND_IMAGE: "docker:19.03.1" DOCKER_DIND_SERVICE: "$DOCKER_DIND_IMAGE-dind" DOCKER_DRIVER: overlay @@ -34,21 +34,14 @@ variables: MCTS_SOLVER_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f zpp-solver/mcts-solver/pom.xml" EMS_CLI: "mvn --batch-mode -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/pom.xml" - # - EMS_UTIL_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/util/pom.xml" - # - EMS_BROKER_CLIENT_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/broker-client/pom.xml" - # - EMS_BROKER_CEP_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/broker-cep/pom.xml" - # - EMS_BAGUETTE_CLIENT_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/baguette-client/pom.xml" - # - EMS_BAGUETTE_CLIENT_INSTALL_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/baguette-client-install/pom.xml" - # - EMS_BAGUETTE_SERVER_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/baguette-server/pom.xml" - # - EMS_TRANSLATOR_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/translator/pom.xml" - EMS_SERVER_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/control-service/pom.xml" + #EMS_UTIL_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/util/pom.xml" + #EMS_BROKER_CLIENT_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/broker-client/pom.xml" + #EMS_BROKER_CEP_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/broker-cep/pom.xml" + #EMS_BAGUETTE_CLIENT_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/baguette-client/pom.xml" + #EMS_BAGUETTE_CLIENT_INSTALL_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/baguette-client-install/pom.xml" + #EMS_BAGUETTE_SERVER_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/baguette-server/pom.xml" + #EMS_TRANSLATOR_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/translator/pom.xml" + #EMS_SERVER_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f event-management/control-service/pom.xml" METASOLVER_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f meta_solver/pom.xml" MQ_ADAPTER_CLI: "mvn --batch-mode -N -Dmaven.test.skip=$SKIP_TESTS -Ddocker.push=false -f mq-http-adapter/pom.xml" diff --git a/event-management/README-for-TESTING.md b/event-management/README-for-TESTING.md new file mode 100644 index 0000000000000000000000000000000000000000..ac63e6ecec8082652ef29542aaf5fa5081c84c4e --- /dev/null +++ b/event-management/README-for-TESTING.md @@ -0,0 +1,1353 @@ +# 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 JRE8 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 `eu.melodic.event.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 JRE8 in RL nodes. +> - EMS server will install Netda 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... + MELODIC_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> MELODIC_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... + MELODIC_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: eu.melodic.event.util.password.AsterisksPasswordEncoder + PasswordUtil: Initialized default Password Encoder: eu.melodic.event.util.password.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

* + ``` + ......................... BUG: No Give up message + ``` + 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-info={random=cecab3d4-4c09-43b1-b6fa-3534d37bbc8f, zone-id=IMU-ZONE, address=192.168.16.4,......................... + 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 #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-info={random=cecab3d4-4c09-43b1-b6fa-3534d37bbc8f, zone-id=IMU-ZONE, address=192.168.16.4,......................... + 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) + ``` + ``` + ......................... BUG: No Give up message + ``` + * ***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> MELODIC_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... + MELODIC_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: eu.melodic.event.util.password.AsterisksPasswordEncoder + PasswordUtil: Initialized default Password Encoder: eu.melodic.event.util.password.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's + +**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","provider":"AWS","name":"vm2","ssh.port":"22","ssh.username":"ubuntu","ssh.password":"ubuntu",......................... + ``` + *

EMS server log: New Aggregator give up message

* + ``` + ......................... BUG: No give up message + ``` + 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 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-info={random=4abf9ae2-b7fc-4e8c-b6d9-464623d1b05f, zone-id=IMU-ZONE, address=192.168.16.4,......................... + 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 #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-info={random=4abf9ae2-b7fc-4e8c-b6d9-464623d1b05f, zone-id=IMU-ZONE, address=192.168.16.4,......................... + 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) + ``` + ``` + ......................... BUG: No give up message + ``` + * ***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: Will pause metrics collection from node for 60 seconds: 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: 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: Node is in ignore list: 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: Node is in ignore list: 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: Node is in ignore list: 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: Node is in ignore list: 192.168.96.2 + + Collectors::Netdata: Resumed metrics collection from node: 192.168.96.2 + SelfHealingPlugin: cancelRecoveryTask(): Cancelled recovery task for Node: id=null, address=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 + ``` + * ***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

* + ``` + ......................... BUG: No Give up message + ``` + * ***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: Will pause metrics collection from node for 60 seconds: 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... + Heartbeat 1645015873205 + SelfHealingPlugin: EXCEPTION while recovering node: node-info={random=4b676a58-e00e-4ddf-a21e-b1c0d1382cd6, zone-id=IMU-ZONE, address=192.168.96.2, provider=AWS,......................... + 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 + ......................... + 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 + + Resumed metrics collection from node: 192.168.96.2 + ncelRecoveryTask(): Cancelled recovery task for Node: id=null, address=192.168.96.2 + + 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] + Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + 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": No route to host (Host unreachable); nested exception is java.net.NoRouteToHostException: No route to host (Host unreachable) -> java.net.NoRouteToHostException: No route to host (Host unreachable) + + 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] + Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + 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": No route to host (Host unreachable); nested exception is java.net.NoRouteToHostException: No route to host (Host unreachable) -> java.net.NoRouteToHostException: No route to host (Host unreachable) + + 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] + Collecting data from url: http://192.168.96.2:19999/api/v1/allmetrics?format=json + 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": No route to host (Host unreachable); nested exception is java.net.NoRouteToHostException: No route to host (Host unreachable) -> java.net.NoRouteToHostException: No route to host (Host unreachable) + Too many consecutive errors occurred while attempting to collect metrics from node: 192.168.96.2, num-of-errors=3 + Will pause metrics collection from node for 60 seconds: 192.168.96.2 + ......................... + ``` + ``` + ......................... BUG: No Give up message + ``` + * ***Normal nodes (that operate)***, for NO logs indicating connection failures or recovery actions. + + + +**B.7) Successful recovery of 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 and Bugs + +* Clustering is never used for 2-level monitoring topologies. +* ***Bug:*** EMS clients do not give up after many recovery failures. -- No message is sent to EMS server for failed recoveries. +* 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/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationProperties.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationProperties.java index eb96902c0ba19e9fff92652f3949c36126a9e6fc..c0a9318621aac59f62a56c74825d5d275469c2dc 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationProperties.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationProperties.java @@ -10,6 +10,7 @@ package eu.melodic.event.baguette.client.install; import lombok.Data; +import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -18,6 +19,7 @@ import org.springframework.context.annotation.PropertySource; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; @Data @Configuration @@ -35,6 +37,7 @@ public class ClientInstallationProperties { private String checkInstalledFile; private String downloadUrl; + @ToString.Exclude private String apiKey; private String installScriptUrl; private String installScriptFile; @@ -66,7 +69,24 @@ public class ClientInstallationProperties { private long commandExecutionTimeout = 60000; private final Map> instructions = new HashMap<>(); + private final Map parameters = new HashMap<>(); 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; } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationTask.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationTask.java index f0270dcd49cea30b43c3ae64bf5e84be67fe122a..96bf004630b13149458aedf198d4642a58dbf2a0 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationTask.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallationTask.java @@ -9,7 +9,8 @@ package eu.melodic.event.baguette.client.install; -import eu.melodic.event.baguette.client.install.instruction.InstallationInstructions; +import eu.melodic.event.baguette.client.install.instruction.InstructionsSet; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import lombok.Builder; import lombok.Data; @@ -29,5 +30,6 @@ public class ClientInstallationTask { private final String type; private final String provider; private final SshConfig ssh; - private final List installationInstructions; + private final NodeRegistryEntry nodeRegistryEntry; + private final List instructionSets; } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstaller.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstaller.java index 1302d5d847282bf70c8942d9f9c0a72a1bd045a6..77179a2879c605c115ccbad407bd6c3b40cbac61 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstaller.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstaller.java @@ -9,6 +9,8 @@ package eu.melodic.event.baguette.client.install; +import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; @@ -31,6 +33,8 @@ public class ClientInstaller implements InitializingBean { @Autowired private ClientInstallationProperties properties; + @Autowired + private BaguetteServer baguetteServer; private final AtomicLong taskCounter = new AtomicLong(); private ExecutorService executorService; @@ -57,8 +61,28 @@ public class ClientInstaller implements InitializingBean { } 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())) { - return executeVmTask(task, taskCounter); + 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); + + 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); + } + + // Pre-register Node to baguette Server Coordinator + baguetteServer.getNodeRegistry().getCoordinator().preregister(entry); + + return success; } else { log.error("ClientInstaller: UNSUPPORTED TASK TYPE: {}", task.getType()); } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallerPlugin.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallerPlugin.java index ff2b5514e852ad63d669bceae1065ca30e28d148..4e6fa33073e79dd1fe180951c4662c67adb6c0e2 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallerPlugin.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/ClientInstallerPlugin.java @@ -10,5 +10,14 @@ package eu.melodic.event.baguette.client.install; public interface ClientInstallerPlugin { - boolean execute(); + 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/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/SshClientInstaller.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/SshClientInstaller.java index 4350cd1a85d5ea4cb4964dbfe2a2c5887fbcd622..3578a3c1e6b468ba6648ff5611a5dbb2cfcfd3bf 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/SshClientInstaller.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/SshClientInstaller.java @@ -9,9 +9,12 @@ package eu.melodic.event.baguette.client.install; -import eu.melodic.event.baguette.client.install.instruction.InstallationInstructions; +import eu.melodic.event.baguette.client.install.instruction.INSTRUCTION_RESULT; import eu.melodic.event.baguette.client.install.instruction.Instruction; +import eu.melodic.event.baguette.client.install.instruction.InstructionsService; +import eu.melodic.event.baguette.client.install.instruction.InstructionsSet; import lombok.Builder; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringSubstitutor; @@ -26,7 +29,6 @@ import org.apache.sshd.client.scp.ScpClient; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.keyprovider.KeyPairProvider; -import org.apache.sshd.common.scp.ScpTimestamp; import org.apache.sshd.common.util.io.NoCloseInputStream; import org.apache.sshd.common.util.io.NoCloseOutputStream; import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey; @@ -40,7 +42,6 @@ import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.PosixFilePermission; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; @@ -48,6 +49,10 @@ import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; 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; @@ -95,7 +100,7 @@ public class SshClientInstaller implements ClientInstallerPlugin { @Builder public SshClientInstaller(ClientInstallationTask task, long taskCounter, ClientInstallationProperties properties) { - this.task= task; + this.task = task; this.taskCounter = taskCounter; this.maxRetries = properties.getMaxRetries()>0 ? properties.getMaxRetries() : 5; @@ -111,9 +116,8 @@ public class SshClientInstaller implements ClientInstallerPlugin { } @Override - public boolean execute() { return executeTask(); } - - private boolean executeTask(/*int retries*/) { + public boolean executeTask(/*int retries*/) { + task.getNodeRegistryEntry().nodeInstalling(task.getNodeRegistryEntry().getPreregistration()); boolean success = false; int retries = 0; while (!success && retries<=maxRetries) { @@ -134,7 +138,8 @@ public class SshClientInstaller implements ClientInstallerPlugin { } try { - success = executeInstructionsList(); + 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; @@ -493,54 +498,104 @@ public class SshClientInstaller implements ClientInstallerPlugin { return true; } - private boolean executeInstructionsList() throws IOException { - List installationInstructionsList = task.getInstallationInstructions(); + private INSTRUCTION_RESULT executeInstructionSets() throws IOException { + List instructionsSetList = task.getInstructionSets(); + INSTRUCTION_RESULT exitResult = INSTRUCTION_RESULT.SUCCESS; int cntSuccess = 0; int cntFail = 0; - for (InstallationInstructions installationInstructions : installationInstructionsList) { - log.info("----------------------------------------------------------------------"); - log.info("SshClientInstaller: Task #{}: Executing installation instructions set: {}", taskCounter, installationInstructions.getDescription()); + 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("----------------------------------------------------------------------\nExecuting instruction set: %s\n", - installationInstructions.getDescription())); - boolean result = executeInstructions(installationInstructions); - if (!result) { - log.error("SshClientInstaller: Task #{}: Installation Instructions failed: {}", taskCounter, installationInstructions.getDescription()); + 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) - return false; + 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 succeeded: {}", taskCounter, installationInstructions.getDescription()); + log.info("SshClientInstaller: Task #{}: Installation Instructions set succeeded: {}", taskCounter, instructionsSet.getDescription()); cntSuccess++; } } - log.info("-------------------------------------------------------------------------"); - log.info("SshClientInstaller: Task #{}: Instruction sets processed: successful={}, failed={}", taskCounter, cntSuccess, cntFail); - return true; + log.info("\n -------------------------------------------------------------------------\n Task #{} : Instruction sets processed: successful={}, failed={}, exit-result={}", taskCounter, cntSuccess, cntFail, exitResult); + return exitResult; } - private boolean executeInstructions(InstallationInstructions installationInstructions) throws IOException { - Map valueMap = installationInstructions.getValueMap(); - int numOfInstructions = installationInstructions.getInstructions().size(); + private INSTRUCTION_RESULT executeInstructions(InstructionsSet instructionsSet) throws IOException { + Map valueMap = task.getNodeRegistryEntry().getPreregistration(); + int numOfInstructions = instructionsSet.getInstructions().size(); int cnt = 0; - int insCount = installationInstructions.getInstructions().size(); - for (Instruction ins : installationInstructions.getInstructions()) { + 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.getDescription()); + log.info("SshClientInstaller: Task #{}: Executing instruction {}/{}: {}", taskCounter, cnt, numOfInstructions, ins.description()); Integer exitStatus; boolean result = true; - switch (ins.getTaskType()) { + switch (ins.taskType()) { case LOG: - log.info("SshClientInstaller: Task #{}: LOG: {}", taskCounter, ins.getMessage()); + log.info("SshClientInstaller: Task #{}: LOG: {}", taskCounter, ins.message()); break; case CMD: - log.info("SshClientInstaller: Task #{}: EXEC: {}", taskCounter, ins.getCommand()); + log.info("SshClientInstaller: Task #{}: EXEC: {}", taskCounter, ins.command()); int retries = 0; - int maxRetries = ins.getRetries(); + int maxRetries = ins.retries(); while (true) { try { - exitStatus = sshExecCmd(ins.getCommand(), ins.getExecutionTimeout()); + exitStatus = sshExecCmd(ins.command(), ins.executionTimeout()); result = (exitStatus!=null); //result = (exitStatus==0); log.info("SshClientInstaller: Task #{}: EXEC: exit-status={}", taskCounter, exitStatus); @@ -555,7 +610,7 @@ public class SshClientInstaller implements ClientInstallerPlugin { retries++; if (retries<=maxRetries) { log.info("SshClientInstaller: Task #{}: Retry {}/{} for instruction {}/{}: {}", - taskCounter, retries, maxRetries, cnt, numOfInstructions, ins.getDescription()); + taskCounter, retries, maxRetries, cnt, numOfInstructions, ins.description()); } else { if (maxRetries>0) log.error("sshClientInstaller: Task #{}: Last instruction failed {} times. Giving up", taskCounter, maxRetries); @@ -594,43 +649,100 @@ public class SshClientInstaller implements ClientInstallerPlugin { break;*/ case FILE: //log.info("SshClientInstaller: Task #{}: FILE: {}, content-length={}", taskCounter, ins.getFileName(), ins.getContents().length()); - if (Paths.get(ins.getLocalFileName()).toFile().isDirectory()) { - log.info("SshClientInstaller: Task #{}: FILE: COPY-PROCESS DIR: {} -> {}", taskCounter, ins.getLocalFileName(), ins.getFileName()); - result = copyDir(ins.getLocalFileName(), ins.getFileName(), valueMap); + 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.getLocalFileName()).toFile().isFile()) { - log.info("SshClientInstaller: Task #{}: FILE: COPY-PROCESS FILE: {} -> {}", taskCounter, ins.getLocalFileName(), ins.getFileName()); - Path sourceFile = Paths.get(ins.getLocalFileName()); - Path sourceBaseDir = Paths.get(ins.getLocalFileName()).getParent(); - result = copyFile(sourceFile, sourceBaseDir, ins.getFileName(), valueMap, ins.isExecutable()); + 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.getLocalFileName()); + log.error("SshClientInstaller: Task #{}: FILE: ERROR: Local file is not directory or normal file: {}", taskCounter, ins.localFileName()); result = false; } break; case COPY: - log.info("SshClientInstaller: Task #{}: UPLOAD: {} -> {}", taskCounter, ins.getLocalFileName(), ins.getFileName()); - result = sshFileUpload(ins.getLocalFileName(), ins.getFileName()); + 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.getCommand()); - exitStatus = sshExecCmd(ins.getCommand()); + 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.isMatch(), ins.getExitCode(), exitStatus); - if (ins.isMatch() && exitStatus==ins.getExitCode() - || !ins.isMatch() && exitStatus!=ins.getExitCode()) + 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.getMessage()); + log.info("SshClientInstaller: Task #{}: CHECK: MATCH: {}", taskCounter, ins.message()); log.info("SshClientInstaller: Task #{}: CHECK: MATCH: Will not process more instructions", taskCounter); - return true; + 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 false; + return INSTRUCTION_RESULT.FAIL; } if (cnt valueMap) throws IOException { @@ -674,4 +786,140 @@ public class SshClientInstaller implements ClientInstallerPlugin { 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) -> { + 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().nodeInstallationComplete(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/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/AbstractInstallationHelper.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/AbstractInstallationHelper.java index 14fbd79ff2f7b5075edae121f856f4450d90e715..3f332ae021c9e37e971aba77605c61e1b702510c 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/AbstractInstallationHelper.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/AbstractInstallationHelper.java @@ -9,11 +9,13 @@ package eu.melodic.event.baguette.client.install.helper; +import com.google.gson.Gson; import eu.melodic.event.baguette.client.install.ClientInstallationProperties; -import eu.melodic.event.baguette.client.install.instruction.InstallationInstructions; -import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.baguette.client.install.instruction.InstructionsSet; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import eu.melodic.event.util.KeystoreUtil; import eu.melodic.event.util.NetUtil; +import eu.melodic.event.util.PasswordUtil; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -53,16 +55,20 @@ public abstract class AbstractInstallationHelper implements InitializingBean, Ap @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; } + public synchronized static AbstractInstallationHelper getInstance() { + return instance; + } @Override public void afterPropertiesSet() { - log.info("AbstractInstallationHelper.afterPropertiesSet(): configuration: {}", properties); + 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"); @@ -109,6 +115,7 @@ public abstract class AbstractInstallationHelper implements InitializingBean, Ap log.debug("AbstractInstallationHelper.initServerCertificate(): Exporting server certificate to file: {}", certFileName); KeystoreUtil .getKeystore(keystoreFile, keystoreType, keystorePassword) + .passwordUtil(passwordUtil) .exportCertToFile(keyAlias, certFileName); log.debug("AbstractInstallationHelper.initServerCertificate(): Server certificate exported"); @@ -119,6 +126,7 @@ public abstract class AbstractInstallationHelper implements InitializingBean, Ap } else { this.serverCert = KeystoreUtil .getKeystore(keystoreFile, keystoreType, keystorePassword) + .passwordUtil(passwordUtil) .getEntryCertificateAsPEM(keyAlias); } @@ -189,27 +197,43 @@ public abstract class AbstractInstallationHelper implements InitializingBean, Ap } } - public List prepareInstallationInstructionsForOs(Map nodeMap, Map contextMap, BaguetteServer baguette) throws IOException { - if (! baguette.isServerRunning()) throw new RuntimeException("Baguette Server is not running"); + 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.size()>0) { + // 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); + } - String baseUrl = contextMap.get("BASE_URL"); - String clientId = contextMap.get("CLIENT_ID"); - String ipSetting = contextMap.get("IP_SETTING"); - log.trace("AbstractInstallationHelper.prepareInstallationInstructionsForOs(): node-map={}, base-url={}, client-id={}", nodeMap, baseUrl, clientId); + 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 = (String) nodeMap.get("operatingSystem"); - List installationInstructionsList = null; + String osFamily = entry.getPreregistration().get("operatingSystem"); + List instructionsSetList = null; if (LINUX_OS_FAMILIES.contains(osFamily.toUpperCase())) - installationInstructionsList = prepareInstallationInstructionsForLinux(nodeMap, contextMap, baguette); + instructionsSetList = prepareInstallationInstructionsForLinux(entry); else if (WINDOWS_OS_FAMILIES.contains(osFamily.toUpperCase())) - installationInstructionsList = prepareInstallationInstructionsForWin(nodeMap, contextMap, baguette); + instructionsSetList = prepareInstallationInstructionsForWin(entry); else log.warn("AbstractInstallationHelper.prepareInstallationInstructionsForOs(): Unsupported OS family: {}", osFamily); - return installationInstructionsList; + return instructionsSetList; } - protected InstallationInstructions _appendCopyInstructions( - InstallationInstructions installationInstructions, + protected InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, Path p, Path startDir, String copyToClientDir, @@ -223,13 +247,13 @@ public abstract class AbstractInstallationHelper implements InitializingBean, Ap String contents = new String(Files.readAllBytes(p)); contents = StringSubstitutor.replace(contents, valueMap); String tmpFile = clientTmpDir+"/installEMS_"+System.currentTimeMillis(); - installationInstructions + instructionsSet .appendLog(String.format("Copy file from server to temp to client: %s -> %s -> %s", p.toString(), tmpFile, targetFile)); - return _appendCopyInstructions(installationInstructions, targetFile, tmpFile, contents, clientTmpDir); + return _appendCopyInstructions(instructionsSet, targetFile, tmpFile, contents, clientTmpDir); } - protected InstallationInstructions _appendCopyInstructions( - InstallationInstructions installationInstructions, + protected InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, String targetFile, String tmpFile, String contents, @@ -238,16 +262,16 @@ public abstract class AbstractInstallationHelper implements InitializingBean, Ap { if (StringUtils.isEmpty(tmpFile)) tmpFile = clientTmpDir+"/installEMS_"+System.currentTimeMillis(); - installationInstructions + instructionsSet .appendWriteFile(tmpFile, contents, false) .appendExec("sudo mv " + tmpFile + " " + targetFile) .appendExec("sudo chmod u+rw,og-rwx " + targetFile); - return installationInstructions; + return instructionsSet; } protected String _prepareUrl(String urlTemplate, String baseUrl) { return urlTemplate - .replace("%{BASE_URL}%", baseUrl) + .replace("%{BASE_URL}%", Optional.ofNullable(baseUrl).orElse("")) .replace("%{PUBLIC_IP}%", Optional.ofNullable(NetUtil.getPublicIpAddress()).orElse("")) .replace("%{DEFAULT_IP}%", Optional.ofNullable(NetUtil.getDefaultIpAddress()).orElse("")); } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelper.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelper.java index 32bfde969ca6a4f92227ac132356ec8d066447b5..a9c06324701c6d00bb72ff400b623c931be9746d 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelper.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelper.java @@ -10,17 +10,19 @@ package eu.melodic.event.baguette.client.install.helper; import eu.melodic.event.baguette.client.install.ClientInstallationTask; -import eu.melodic.event.baguette.client.install.instruction.InstallationInstructions; -import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.baguette.client.install.instruction.InstructionsSet; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import java.io.IOException; import java.util.List; -import java.util.Map; +import java.util.Optional; public interface InstallationHelper { - List prepareInstallationInstructionsForOs(Map nodeMap, Map contextMap, BaguetteServer baguette) throws IOException; - List prepareInstallationInstructionsForWin(Map nodeMap, Map contextMap, BaguetteServer baguette); - List prepareInstallationInstructionsForLinux(Map nodeMap, Map contextMap, BaguetteServer baguette) throws IOException; + Optional> getInstallationInstructionsForOs(NodeRegistryEntry entry) throws IOException; - ClientInstallationTask createClientInstallationTask(Map nodeMap, Map contextMap, BaguetteServer baguette) throws Exception; + List prepareInstallationInstructionsForOs(NodeRegistryEntry entry) throws IOException; + List prepareInstallationInstructionsForWin(NodeRegistryEntry entry); + List prepareInstallationInstructionsForLinux(NodeRegistryEntry entry) throws IOException; + + ClientInstallationTask createClientInstallationTask(NodeRegistryEntry entry) throws Exception; } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelperFactory.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelperFactory.java index 3cadb50e4fc5960ac00bb1239ba319512d0bffaf..f7f5f0f6b4b4ec3f7b0d35ae4e7c3fa5d07387c3 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelperFactory.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/InstallationHelperFactory.java @@ -9,6 +9,7 @@ package eu.melodic.event.baguette.client.install.helper; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; @@ -36,15 +37,15 @@ public class InstallationHelperFactory implements InitializingBean { InstallationHelperFactory.instance = this; } - public InstallationHelper createInstallationHelper(Map nodeMap) { - String nodeType = (String) nodeMap.get("type"); + public InstallationHelper createInstallationHelper(NodeRegistryEntry entry) { + String nodeType = entry.getPreregistration().get("type"); if ("VM".equalsIgnoreCase(nodeType)) { - return createVmInstallationHelper(nodeMap); + return createVmInstallationHelper(entry); } throw new IllegalArgumentException("Unsupported or missing Node type: "+nodeType); } - public InstallationHelper createInstallationHelperBean(String className, Map nodeMap) throws ClassNotFoundException { + public InstallationHelper createInstallationHelperBean(String className, NodeRegistryEntry entry) throws ClassNotFoundException { Class clzz = Class.forName(className); return (InstallationHelper) applicationContext.getBean(clzz); } @@ -56,7 +57,7 @@ public class InstallationHelperFactory implements InitializingBean { return (InstallationHelper) clzz.getDeclaredMethod("getInstance").invoke(null); } - private InstallationHelper createVmInstallationHelper(Map nodeMap) { + private InstallationHelper createVmInstallationHelper(NodeRegistryEntry entry) { return VmInstallationHelper.getInstance(); } } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/VmInstallationHelper.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/VmInstallationHelper.java index 31f5a2286f8dfbc108180189313f7daf18a0146d..d72f9ab26fa0845576a5d2f487b67e317844e894 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/VmInstallationHelper.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/helper/VmInstallationHelper.java @@ -11,17 +11,18 @@ package eu.melodic.event.baguette.client.install.helper; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import eu.melodic.event.baguette.client.install.ClientInstallationProperties; import eu.melodic.event.baguette.client.install.ClientInstallationTask; import eu.melodic.event.baguette.client.install.SshConfig; -import eu.melodic.event.baguette.client.install.instruction.InstallationInstructions; +import eu.melodic.event.baguette.client.install.instruction.InstructionsSet; import eu.melodic.event.baguette.client.install.instruction.Instruction; import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import eu.melodic.event.util.CredentialsMap; 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.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.util.FileCopyUtils; @@ -32,6 +33,7 @@ import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; @@ -41,25 +43,35 @@ import java.util.stream.Collectors; @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 Environment environment; + private ClientInstallationProperties clientInstallationProperties; @Override - public ClientInstallationTask createClientInstallationTask(Map nodeMap, Map contextMap, BaguetteServer baguette) throws IOException { - String baseUrl = contextMap.get("BASE_URL"); - String clientId = contextMap.get("CLIENT_ID"); - String ipSetting = contextMap.get("IP_SETTING"); + public ClientInstallationTask createClientInstallationTask(NodeRegistryEntry entry) 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 = (String) nodeMap.get("id"); - String nodeOs = (String) nodeMap.get("operatingSystem"); - String nodeAddress = (String) nodeMap.get("address"); - String nodeType = (String) nodeMap.get("type"); - String nodeName = (String) nodeMap.get("name"); - String nodeProvider = (String) nodeMap.get("provider"); + 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"; @@ -67,26 +79,23 @@ public class VmInstallationHelper extends AbstractInstallationHelper { if (StringUtils.isBlank(nodeAddress)) throw new IllegalArgumentException("Missing Address for Node"); // Extract node SSH information - Object sshObj = nodeMap.get("ssh"); - if (sshObj==null) throw new IllegalArgumentException("Missing SSH info for Node"); - if (!(sshObj instanceof Map)) throw new IllegalArgumentException("SSH info for Node is *not* a Map"); - - Map nodeSsh = (Map) nodeMap.get("ssh"); - int port = (int) Double.parseDouble(Objects.toString(nodeSsh.get("port"), "22")); - String username = (String) nodeSsh.get("username"); - String password = (String) nodeSsh.get("password"); - String privateKey = (String) nodeSsh.get("key"); - String fingerprint = (String) nodeSsh.get("fingerprint"); - + int port = (int) Double.parseDouble(Objects.toString(nodeMap.get("ssh.port"), "22")); if (port<1) port = 22; - - if (StringUtils.isBlank(username)) throw new IllegalArgumentException("Missing username for SSH"); - if ((password == null || password.isEmpty()) && StringUtils.isBlank(privateKey)) + 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 installationInstructionsList = - prepareInstallationInstructionsForOs(nodeMap, contextMap, baguette); + List instructionsSetList = + prepareInstallationInstructionsForOs(entry); // Create Installation Task for VM node ClientInstallationTask installationTask = ClientInstallationTask.builder() @@ -105,23 +114,28 @@ public class VmInstallationHelper extends AbstractInstallationHelper { .build()) .type(nodeType) .provider(nodeProvider) - .installationInstructions(installationInstructionsList) + .instructionSets(instructionsSetList) + .nodeRegistryEntry(entry) .build(); + log.debug("VmInstallationHelper.createClientInstallationTask(): Created client installation task: {}", installationTask); return installationTask; } @Override - public List prepareInstallationInstructionsForWin(Map nodeMap, Map contextMap, BaguetteServer baguette) { + public List prepareInstallationInstructionsForWin(NodeRegistryEntry entry) { log.warn("VmInstallationHelper.prepareInstallationInstructionsForWin(): NOT YET IMPLEMENTED"); throw new IllegalArgumentException("VmInstallationHelper.prepareInstallationInstructionsForWin(): NOT YET IMPLEMENTED"); } @Override - public List prepareInstallationInstructionsForLinux(Map nodeMap, Map contextMap, BaguetteServer baguette) throws IOException { - String baseUrl = contextMap.get("BASE_URL"); - String clientId = contextMap.get("CLIENT_ID"); - String ipSetting = contextMap.get("IP_SETTING"); + public List prepareInstallationInstructionsForLinux(NodeRegistryEntry entry) throws IOException { + Map nodeMap = entry.getPreregistration(); + BaguetteServer baguette = entry.getBaguetteServer(); + + String baseUrl = 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 @@ -141,38 +155,48 @@ public class VmInstallationHelper extends AbstractInstallationHelper { String clientTmpDir = StringUtils.firstNonBlank(properties.getClientTmpDir(), "/tmp"); - // Initialize values map with nodeMap (from request) - Map valueMap = new HashMap<>(nodeMap.entrySet().stream() - .filter(e -> e.getValue() instanceof String) - .collect(Collectors.toMap(e -> "NODE_"+e.getKey().toUpperCase(), e -> (String)e.getValue()))); - valueMap.putAll( ((Map)nodeMap.get("ssh")).entrySet().stream() - .filter(e -> e.getValue() instanceof String) - .collect(Collectors.toMap(e -> "NODE_SSH_"+e.getKey().toUpperCase(), e -> (String)e.getValue()))); + // 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)); + nodeMap.putAll(additionalKeysMap); // Load client config. template and prepare configuration - valueMap.put("BAGUETTE_CLIENT_ID", clientId); - valueMap.put("BAGUETTE_SERVER_ADDRESS", baguette.getConfiguration().getServerAddress()); - valueMap.put("BAGUETTE_SERVER_HOSTNAME", baguette.getConfiguration().getServerHostname()); - valueMap.put("BAGUETTE_SERVER_PORT", ""+baguette.getConfiguration().getServerPort()); - valueMap.put("BAGUETTE_SERVER_PUBKEY", baguette.getServerPubkey()); - valueMap.put("BAGUETTE_SERVER_PUBKEY_FINGERPRINT", baguette.getServerPubkeyFingerprint()); + nodeMap.put("BAGUETTE_CLIENT_ID", clientId); + nodeMap.put("BAGUETTE_SERVER_ADDRESS", baguette.getConfiguration().getServerAddress()); + nodeMap.put("BAGUETTE_SERVER_HOSTNAME", baguette.getConfiguration().getServerHostname()); + nodeMap.put("BAGUETTE_SERVER_PORT", ""+baguette.getConfiguration().getServerPort()); + nodeMap.put("BAGUETTE_SERVER_PUBKEY", baguette.getServerPubkey()); + nodeMap.put("BAGUETTE_SERVER_PUBKEY_FINGERPRINT", baguette.getServerPubkeyFingerprint()); CredentialsMap.Entry pair = - baguette.getConfiguration().getCredentials().entrySet().iterator().next(); - valueMap.put("BAGUETTE_SERVER_USERNAME", pair.getKey()); - valueMap.put("BAGUETTE_SERVER_PASSWORD", pair.getValue()); + 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"); - valueMap.put("IP_SETTING", ipSetting); + nodeMap.put("IP_SETTING", ipSetting); // Misc. installation property values - valueMap.put("BASE_URL", baseUrl); - valueMap.put("DOWNLOAD_URL", baseDownloadUrl); - valueMap.put("API_KEY", apiKey); - valueMap.put("SERVER_CERT_FILE", serverCertFile); - valueMap.put("REMOTE_TMP_DIR", clientTmpDir); - - valueMap.put("EMS_PUBLIC_DIR", System.getProperty("PUBLIC_DIR", System.getenv("PUBLIC_DIR"))); - log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: value-map: {}", valueMap); + 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) { @@ -189,58 +213,60 @@ public class VmInstallationHelper extends AbstractInstallationHelper { .sorted() .collect(Collectors.toList()); for (Path p : paths) { - _appendCopyInstructions(installationInstructions, p, startDir, copyToClientDir, clientTmpDir, valueMap); + _appendCopyInstructions(instructionSets, p, startDir, copyToClientDir, clientTmpDir, valueMap); } } }*/ - List installationInstructionsList = new ArrayList<>(); + List instructionsSetList = new ArrayList<>(); try { // Read installation instructions from JSON file - List jsonFiles = properties.getInstructions().get("LINUX"); + List jsonFiles = null; + if (nodeMap.containsKey("instruction-files")) { + jsonFiles = Arrays.stream(nodeMap.getOrDefault("instruction-files", "").toString().split(",")) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); + if (jsonFiles.size()==0) + log.warn("VmInstallationHelper.prepareInstallationInstructionsForLinux: Context map contains 'instruction-files' entry with no contents"); + } else { + jsonFiles = properties.getInstructions().get("LINUX"); + } for (String jsonFile : jsonFiles) { log.debug("VmInstallationHelper.prepareInstallationInstructionsForLinux: Installation instructions file for LINUX: {}", jsonFile); byte[] bdata = FileCopyUtils.copyToByteArray(resourceLoader.getResource(jsonFile).getInputStream()); String json = new String(bdata, StandardCharsets.UTF_8); log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: Template installation instructions for LINUX: json:\n{}", json); - // Process placeholders - json = StringSubstitutor.replace(json, valueMap); - json = environment.resolvePlaceholders(json); - //json = environment.resolveRequiredPlaceholders(json); - json = json.replace('\\', '/'); - log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: Installation instructions for LINUX after placeholder processing: json:\n{}", json); - - // Create InstallationInstructions object from JSON - InstallationInstructions installationInstructions = - new Gson().fromJson(json, InstallationInstructions.class); - installationInstructions.setValueMap(valueMap); - installationInstructions.setFileName(jsonFile); - log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: Installation instructions for LINUX: object:\n{}", installationInstructions); - - // Pretty print installationInstructions JSON + // Create InstructionsSet object from JSON + InstructionsSet instructionsSet = + new Gson().fromJson(json, InstructionsSet.class); + instructionsSet.setFileName(jsonFile); + log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: Installation instructions for LINUX: object:\n{}", instructionsSet); + + // Pretty print instructionsSet JSON if (log.isTraceEnabled()) { Gson gson = new GsonBuilder().setPrettyPrinting().create(); StringWriter sw = new StringWriter(); try (PrintWriter writer = new PrintWriter(sw)) { - gson.toJson(installationInstructions, writer); + gson.toJson(instructionsSet, writer); } log.trace("VmInstallationHelper.prepareInstallationInstructionsForLinux: Installation instructions for LINUX: json:\n{}", sw.toString()); } - installationInstructionsList.add(installationInstructions); + instructionsSetList.add(instructionsSet); } - return installationInstructionsList; + return instructionsSetList; } catch (Exception ex) { log.error("VmInstallationHelper.prepareInstallationInstructionsForLinux: Exception while reading Installation instructions for LINUX: ", ex); throw ex; } } - private InstallationInstructions _appendCopyInstructions( - InstallationInstructions installationInstructions, + private InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, Path path, Path localBaseDir, String remoteTargetDir, @@ -253,18 +279,18 @@ public class VmInstallationHelper extends AbstractInstallationHelper { 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(installationInstructions, targetFile, description, contents); + return _appendCopyInstructions(instructionsSet, targetFile, description, contents); } - private InstallationInstructions _appendCopyInstructions( - InstallationInstructions installationInstructions, + private InstructionsSet _appendCopyInstructions( + InstructionsSet instructionsSet, String targetFile, String description, String contents) { - installationInstructions + instructionsSet .appendInstruction(Instruction.createWriteFile(targetFile, contents, false).description(description)) .appendExec("sudo chmod u+rw,og-rwx " + targetFile); - return installationInstructions; + return instructionsSet; } } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/AbstractInstructionsBase.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/AbstractInstructionsBase.java new file mode 100644 index 0000000000000000000000000000000000000000..8765864050ba2904ac0b8125d267ef607711bfe4 --- /dev/null +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/AbstractInstructionsBase.java @@ -0,0 +1,18 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.install.instruction; + +import lombok.Data; + +@Data +public abstract class AbstractInstructionsBase { + private String condition; + private boolean stopOnConditionFail; +} diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/INSTRUCTION_RESULT.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/INSTRUCTION_RESULT.java new file mode 100644 index 0000000000000000000000000000000000000000..c60d1d24fddd45fecd6e76b25db40afc854d86a9 --- /dev/null +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/INSTRUCTION_RESULT.java @@ -0,0 +1,12 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.install.instruction; + +public enum INSTRUCTION_RESULT { SUCCESS, FAIL, EXIT } diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/INSTRUCTION_TYPE.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/INSTRUCTION_TYPE.java index bdc63c2a7026f96b6cad951a248c3359943bbf7e..3a5418d756655728becfa2bcb2e649b4c60d3947 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/INSTRUCTION_TYPE.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/INSTRUCTION_TYPE.java @@ -9,4 +9,7 @@ package eu.melodic.event.baguette.client.install.instruction; -public enum INSTRUCTION_TYPE { LOG, CHECK, CMD, SHELL, FILE, COPY } +public enum INSTRUCTION_TYPE { + LOG, CHECK, CMD, SHELL, FILE, COPY, UPLOAD, DOWNLOAD, + SET_VARS, UNSET_VARS, PRINT_VARS, EXIT, EXIT_SET +} diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/Instruction.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/Instruction.java index 8eb91d855dcafba1da3bcb073b6c5c1acd40ca21..b70324176ead9cb92cdfe93f00a65aa63cf28a70 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/Instruction.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/Instruction.java @@ -11,12 +11,16 @@ package eu.melodic.event.baguette.client.install.instruction; import lombok.Builder; import lombok.Data; +import lombok.experimental.Accessors; import javax.validation.constraints.NotNull; +import java.util.Map; +import java.util.regex.Pattern; @Data -@Builder -public class Instruction { +@Accessors(chain = true, fluent = true) +@Builder(toBuilder = true) +public class Instruction extends AbstractInstructionsBase { private INSTRUCTION_TYPE taskType; private String description; private String message; @@ -30,19 +34,11 @@ public class Instruction { private long executionTimeout; private int retries; - // Fluent API - public Instruction taskType(INSTRUCTION_TYPE taskType) { this.taskType = taskType; return this; } - public Instruction description(String description) { this.description = description; return this; } - public Instruction message(String message) { this.message = message; return this; } - public Instruction command(String command) { this.command = command; return this; } - public Instruction fileName(String fileName) { this.fileName = fileName; return this; } - public Instruction localFileName(String localFileName) { this.localFileName = localFileName; return this; } - public Instruction contents(String contents) { this.contents = contents; return this; } - public Instruction executable(boolean executable) { this.executable = executable; return this; } - public Instruction exitCode(int exitCode) { this.exitCode = exitCode; return this; } - public Instruction match(boolean match) { this.match = match; return this; } - public Instruction executionTimeout(long executionTimeout) { this.executionTimeout = executionTimeout; return this; } - public Instruction retries(int retries) { this.retries = retries; return this; } + 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) { @@ -76,6 +72,14 @@ public class Instruction { .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) diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstructionsService.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstructionsService.java new file mode 100644 index 0000000000000000000000000000000000000000..f7c479921b79f31b5c63ed138aed239e8bc93d73 --- /dev/null +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstructionsService.java @@ -0,0 +1,73 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.install.instruction; + +import lombok.NonNull; +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.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Slf4j +@Service +public class InstructionsService implements EnvironmentAware { + private Environment environment; + 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) { + String condition = i.getCondition(); + if (StringUtils.isBlank(condition)) return true; + String conditionResolved = processPlaceholders(condition, valueMap); + final ExpressionParser parser = new SpelExpressionParser(); + Object result = parser.parseExpression(conditionResolved).getValue(); + 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; + } +} \ No newline at end of file diff --git a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstallationInstructions.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstructionsSet.java similarity index 68% rename from event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstallationInstructions.java rename to event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstructionsSet.java index ff1d059aa2267778d0b1139df88457157c8d44c1..d173c013187e3e441324bb152cce63d5fcd6a31e 100644 --- a/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstallationInstructions.java +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/install/instruction/InstructionsSet.java @@ -14,49 +14,53 @@ import lombok.Data; import java.util.*; @Data -public class InstallationInstructions { +public class InstructionsSet extends AbstractInstructionsBase { private String os; private String description; private String fileName; - private Map valueMap = new HashMap<>(); private List instructions = new ArrayList<>(); - public Map getValueMap() { + /*public Map getValueMap() { return Collections.unmodifiableMap(valueMap); } public void setValueMap(Map valueMap) { this.valueMap = new HashMap<>(valueMap); - } + }*/ public List getInstructions() { return Collections.unmodifiableList(instructions); } public void setInstructions(List ni) { instructions = new ArrayList<>(ni); } - public InstallationInstructions appendInstruction(Instruction i) { + public InstructionsSet appendInstruction(Instruction i) { instructions.add(i); return this; } - public InstallationInstructions appendLog(String message) { + public InstructionsSet appendLog(String message) { instructions.add(Instruction.createLog(message)); return this; } - public InstallationInstructions appendExec(String command) { + public InstructionsSet appendExec(String command) { instructions.add(Instruction.createShellCommand(command)); return this; } - public InstallationInstructions appendWriteFile(String file, String contents, boolean executable) { + public InstructionsSet appendWriteFile(String file, String contents, boolean executable) { instructions.add(Instruction.createWriteFile(file, contents, executable)); return this; } - public InstallationInstructions appendUploadFile(String localFile, String remoteFile) { + public InstructionsSet appendUploadFile(String localFile, String remoteFile) { instructions.add(Instruction.createUploadFile(localFile, remoteFile)); return this; } - public InstallationInstructions appendCheck(String command, int exitCode, boolean match, String message) { + 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/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/selfhealing/ClientRecoveryPlugin.java b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/selfhealing/ClientRecoveryPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..0e7ad43b6b5a5126fb7f3d11e46cf958f816da63 --- /dev/null +++ b/event-management/baguette-client-install/src/main/java/eu/melodic/event/baguette/client/selfhealing/ClientRecoveryPlugin.java @@ -0,0 +1,111 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.selfhealing; + +import eu.melodic.event.baguette.client.install.ClientInstallationProperties; +import eu.melodic.event.baguette.client.install.ClientInstallationTask; +import eu.melodic.event.baguette.client.install.SshClientInstaller; +import eu.melodic.event.baguette.client.install.helper.InstallationHelperFactory; +import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.baguette.server.NodeRegistry; +import eu.melodic.event.baguette.server.NodeRegistryEntry; +import eu.melodic.event.util.EventBus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import java.time.Instant; + +@Slf4j +@Service +@ConditionalOnProperty(name = "CLIENT_RECOVERY_ENABLED", 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 BaguetteServer baguetteServer; + + @Value("${CLIENT_RECOVERY_DELAY:10000}") + private long clientRecoveryDelay; + @Value("${CLIENT_RECOVERY_INSTRUCTIONS_FILES:file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/recover-baguette.json}") + private String recoveryInstructionsFile; + + private final static String CLIENT_EXIT_TOPIC = "BAGUETTE_SERVER_CLIENT_EXITED"; + + @Override + public void afterPropertiesSet() throws Exception { + eventBus.subscribe(CLIENT_EXIT_TOPIC, this); + log.info("ClientRecoveryPlugin: Subscribed for BAGUETTE_SERVER_CLIENT_EXITED 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); + if (CLIENT_EXIT_TOPIC.equals(topic)) { + log.debug("ClientRecoveryPlugin: onMessage(): CLIENT EXITED: message={}", message); + processExitEvent(message, sender); + } + } + + private void processExitEvent(Object message, Object sender) { + log.debug("ClientRecoveryPlugin: processExitEvent(): BEGIN: message={}", message); + if (message instanceof ClientShellCommand) { + ClientShellCommand csc = (ClientShellCommand)message; + String clientId = csc.getId(); + String address = csc.getClientIpAddress(); + log.warn("ClientRecoveryPlugin: processExitEvent(): client-id={}, client-address={}", clientId, address); + NodeRegistryEntry nodeInfo = nodeRegistry.getNodeByAddress(address); + log.debug("ClientRecoveryPlugin: processExitEvent(): client-node-info={}", nodeInfo); + log.trace("ClientRecoveryPlugin: processExitEvent(): node-registry.node-addresses={}", nodeRegistry.getNodeAddresses()); + log.trace("ClientRecoveryPlugin: processExitEvent(): node-registry.nodes={}", nodeRegistry.getNodes()); + taskScheduler.schedule(() -> { + try { + runClientRecovery(nodeInfo); + } catch (Exception e) { + log.error("ClientRecoveryPlugin: processExitEvent(): EXCEPTION: while recovering node: node-info={} -- Exception: ", nodeInfo, e); + } + }, Instant.now().plusMillis(clientRecoveryDelay)); + } else { + log.warn("ClientRecoveryPlugin: processExitEvent(): Message is not a {} object. Will ignore it.", ClientShellCommand.class.getSimpleName()); + } + } + + public void runClientRecovery(NodeRegistryEntry entry) throws Exception { + log.debug("ClientRecoveryPlugin: runClientRecovery(): node-info={}", entry); + if (entry==null) return; + + 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.warn("ClientRecoveryPlugin: runClientRecovery(): Starting client recovery: node-info={}", entry); + boolean result = installer.execute(); + log.warn("ClientRecoveryPlugin: runClientRecovery(): Client recovery completed: result={}, node-info={}", result, entry); + } +} diff --git a/event-management/baguette-client/bin/kill.sh b/event-management/baguette-client/bin/kill.sh index 6baf2d68558df2631b266cd526c25cb04d8d22a6..f5446b54a55ae8e7874024a953eab2219893a330 100755 --- a/event-management/baguette-client/bin/kill.sh +++ b/event-management/baguette-client/bin/kill.sh @@ -16,7 +16,7 @@ BASEDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) # Kill Baguette client #PID=`jps | grep BaguetteClient | cut -d " " -f 1` -PID=`ps -ef |grep java |grep BaguetteClient | cut -c 10-14` +PID=`ps -ef |grep java |grep BaguetteClient | cut -c 10-20` if [ "$PID" != "" ] then echo "Killing baguette client (pid: $PID)" diff --git a/event-management/baguette-client/conf/baguette-client.properties b/event-management/baguette-client/conf/baguette-client.properties index e87ed088fd17c714e2969d2e35ce059ee734bc4e..dded9204152a395d3efaf9e9a1472708d0afd321 100644 --- a/event-management/baguette-client/conf/baguette-client.properties +++ b/event-management/baguette-client/conf/baguette-client.properties @@ -32,6 +32,17 @@ 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 # ----------------------------------------------------------------------------- @@ -41,8 +52,11 @@ server-password = ${BAGUETTE_SERVER_PASSWORD} 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.error-limit = 3 +collector.netdata.pause-period = 60 # ----------------------------------------------------------------------------- # Cluster settings @@ -58,7 +72,7 @@ collector.netdata.url = http://127.0.0.1:19999/api/v1/allmetrics?format=json #cluster.failureTimeout=5000 cluster.testInterval=5000 -cluster.log-enabled=false +cluster.log-enabled=true cluster.out-enabled=true cluster.join-on-init=true diff --git a/event-management/baguette-client/conf/baguette.json b/event-management/baguette-client/conf/baguette.json new file mode 100644 index 0000000000000000000000000000000000000000..cdd4ab4aa6c12b51c21128fba64dfb86e5db0dff --- /dev/null +++ b/event-management/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": "/opt/baguette-client/bin/kill.sh", + "waitBefore": 0, + "waitAfter": 2000 +}, { + "name": "Sending baguette client start command...", + "command": "/opt/baguette-client/bin/run.sh", + "waitBefore": 0, + "waitAfter": 10000 +}] diff --git a/event-management/baguette-client/conf/eu.melodic.event.brokercep.properties b/event-management/baguette-client/conf/eu.melodic.event.brokercep.properties index ca522075077176d1078de8de3e834f238fc8f5b8..f6f46317cb7c151611a73b8a4ded970da62c366d 100644 --- a/event-management/baguette-client/conf/eu.melodic.event.brokercep.properties +++ b/event-management/baguette-client/conf/eu.melodic.event.brokercep.properties @@ -7,7 +7,9 @@ # https://www.mozilla.org/en-US/MPL/2.0/ # -password-encoder-class = eu.melodic.event.util.password.IdentityPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.AsterisksPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.IdentityPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.PresentPasswordEncoder # Broker ports and protocol brokercep.broker-name = broker @@ -52,8 +54,8 @@ brokercep.ssl.key-entry-ext-san = dns:localhost,ip:127.0.0.1,ip:%{DEFAULT_IP}%,i # Authentication and Authorization settings brokercep.authentication-enabled = true -#brokercep.additional-broker-credentials = aaa/111, bbb/222 -brokercep.additional-broker-credentials = ENC(KYZnHeuoJ0NsE1OuIdDKWIHv8shUdcxXZmNtXjXJZdw=) +#brokercep.additional-broker-credentials = aaa/111, bbb/222, morphemic/morphemic +brokercep.additional-broker-credentials = ENC(axeJUxNHajYfBffUwvuT3kwTgLTpRliDMz/ZQ9hROZ3BNOv0Idw72NJsawzIZRuZ) brokercep.authorization-enabled = false # Broker instance settings diff --git a/event-management/baguette-client/conf/logback-spring.xml b/event-management/baguette-client/conf/logback-spring.xml index 437c93a14e4f47f0566b9e8e6001bfbb350e9ee3..f3a0d569d3c842d9652801c55bcfd4e5ad98d0fa 100644 --- a/event-management/baguette-client/conf/logback-spring.xml +++ b/event-management/baguette-client/conf/logback-spring.xml @@ -32,7 +32,7 @@ - + diff --git a/event-management/baguette-client/conf/netdata.json b/event-management/baguette-client/conf/netdata.json new file mode 100644 index 0000000000000000000000000000000000000000..ed40f8260940dd4eeffdfcbd4266f8fcf8c61de2 --- /dev/null +++ b/event-management/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/event-management/baguette-client/pom.xml b/event-management/baguette-client/pom.xml index 35f49c3922f4494ffac8965153ad32f2983854a4..56dbe0a24bde3bf57f4074afe03b942a8a67cb3d 100644 --- a/event-management/baguette-client/pom.xml +++ b/event-management/baguette-client/pom.xml @@ -34,6 +34,11 @@ broker-client ${project.version} + + eu.melodic.event + common + ${project.version} + diff --git a/event-management/baguette-client/src/main/assembly/baguette-client-installation-package.xml b/event-management/baguette-client/src/main/assembly/baguette-client-installation-package.xml index f3491d5dfc22fad3204dab24fa63eb3997565a8b..9c70619122114a20086d8cffcbb9ce4cfebe7890 100644 --- a/event-management/baguette-client/src/main/assembly/baguette-client-installation-package.xml +++ b/event-management/baguette-client/src/main/assembly/baguette-client-installation-package.xml @@ -61,6 +61,22 @@ *.jar + + target + ${project.parent.basedir}/broker-client/target + + broker-client-jar-with-dependencies.jar + + + + bin + ${project.parent.basedir}/broker-client + + client.* + + unix + 0755 + diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/BaguetteClient.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/BaguetteClient.java index 7603bd1f691a88f92f920169d2af4a36992ecd8f..a121b5b089a1678e7126de9fa1e95e3b2fb2ae20 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/BaguetteClient.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/BaguetteClient.java @@ -12,16 +12,21 @@ package eu.melodic.event.baguette.client; import edu.emory.mathcs.backport.java.util.Collections; import eu.melodic.event.baguette.client.cluster.ClusterManagerProperties; import eu.melodic.event.baguette.client.collector.netdata.NetdataCollector; +import eu.melodic.event.baguette.client.plugin.recovery.SelfHealingPlugin; +import eu.melodic.event.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.ApplicationContext; 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; @@ -31,8 +36,9 @@ import java.util.List; * Baguette client */ @Slf4j +@EnableScheduling @SpringBootApplication(scanBasePackages = { - "eu.melodic.event.baguette.client", "eu.melodic.event.brokercep", + "eu.melodic.event.baguette.client", "eu.melodic.event.brokercep", "eu.melodic.event.common", "eu.melodic.event.brokerclient", "eu.melodic.event.util"}) @RequiredArgsConstructor public class BaguetteClient implements ApplicationRunner { @@ -45,12 +51,21 @@ public class BaguetteClient implements ApplicationRunner { 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"); @@ -66,21 +81,23 @@ public class BaguetteClient implements ApplicationRunner { // 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(applicationContext); + runCli(); } else { // Run SSH client log.debug("BaguetteClient: Enters SSH mode"); - runSshClient(applicationContext); + runSshClient(); } log.debug("BaguetteClient: Exiting"); // Stop measurement collectors if (!interactiveMode) { + applicationContext.getBean(SelfHealingPlugin.class).stop(); stopCollectors(); } @@ -142,20 +159,16 @@ public class BaguetteClient implements ApplicationRunner { collectorsList.clear(); } - protected void runSshClient(ApplicationContext appCtx) { + protected void runSshClient() { boolean retry = true; while (true) { try { - log.trace("BaguetteClient: spring-boot application-context: {}", appCtx); - Sshc client = appCtx.getBean(Sshc.class); - client.setConfiguration(baguetteClientProperties); - log.trace("BaguetteClient: Sshc instance from application-context: {}", client); - log.trace("BaguetteClient: Calling SSHC start()"); - client.start(retry); + startSshClient(retry); + log.trace("BaguetteClient: Calling SSHC run()"); client.run(); - log.trace("BaguetteClient: Calling SSHC stop()"); - client.stop(); + + stopSshClient(); } catch (Exception ex) { log.error("BaguetteClient: EXCEPTION: ", ex); } @@ -164,12 +177,30 @@ public class BaguetteClient implements ApplicationRunner { } } - protected void runCli(ApplicationContext appCtx) throws IOException { - BaguetteClientCLI cli = appCtx.getBean(BaguetteClientCLI.class); + 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 { diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Collector.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Collector.java index 2a5629054e9e0df8c7ac24a4f8dcc521c6b1be3b..123b36904d0f81ea7aa015deb6f581c5a558eba5 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Collector.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Collector.java @@ -9,8 +9,8 @@ package eu.melodic.event.baguette.client; -public interface Collector { - void start(); - void stop(); +import eu.melodic.event.util.Plugin; + +public interface Collector extends Plugin { void activeGroupingChanged(String oldGrouping, String newGrouping); } diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/CommandExecutor.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/CommandExecutor.java index eaf545625b5ca8b3ea4861b11f24ccfac7ddceac..dc6ffde6a5988e70c9265fbd49996545f02fd006 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/CommandExecutor.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/CommandExecutor.java @@ -20,12 +20,14 @@ import eu.melodic.event.brokerclient.BrokerClient; import eu.melodic.event.brokerclient.event.EventGenerator; import eu.melodic.event.brokerclient.properties.BrokerClientProperties; import eu.melodic.event.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.stereotype.Service; import java.io.*; @@ -57,6 +59,11 @@ public class CommandExecutor { 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 @@ -65,6 +72,9 @@ public class CommandExecutor { private BrokerClientProperties brokerClientProperties; @Autowired private PasswordUtil passwordUtil; + @Autowired + @Getter + private EventBus eventBus; private BaguetteClientProperties config; private String idFile; @@ -74,6 +84,8 @@ public class CommandExecutor { private PrintStream err; private String clientId; + @Getter + private ClientConfiguration clientConfiguration; @Getter private final Map groupings = new LinkedHashMap<>(); private GroupingConfiguration activeGrouping; @@ -97,6 +109,10 @@ public class CommandExecutor { @Getter private String aggregatorGrouping; @Getter private String nodeGrouping; + private Thread serverWatcherThread; + private boolean captureInputLine; + @Getter private String lastInputLine; + public CommandExecutor() { initializeClientId(); @@ -122,6 +138,46 @@ public class CommandExecutor { } } + void communicateWithServer(InputStream in, PrintStream out, PrintStream err) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line; + while ((line = reader.readLine()) != null) { + if (captureInputLine) { + lastInputLine = line; + captureInputLine = false; + continue; + } + line = line.trim(); + if (StringUtils.startsWithIgnoreCase(line, "CLUSTER-KEY")) { + String[] s = line.split(" ", 2); + log.info("{} {}", s[0], s.length>1 ? passwordUtil.encodePassword(s[1]) : ""); + } else + log.info(line); + try { + boolean exit = execCmd(line.split("[ \t]+"), in, out, err); + if (exit) break; + } catch (Exception ex) { + log.error("", ex); + // Report exception back to server + err.println(ex); + ex.printStackTrace(err); + err.flush(); + } + } + } + + 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); } @@ -146,6 +202,46 @@ public class CommandExecutor { 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; i statsMap = brokerCepService.getBrokerCepStatistics(); - log.info("Statistics: {}", statsMap); + log.debug("Statistics: {}", statsMap); if (out!=null) out.println("-INPUT:"+inputUuid+":"+serializeToString(statsMap)); } @@ -1091,6 +1235,14 @@ public class CommandExecutor { if (out!=null) out.println("STATISTICS CLEARED"); } + public boolean isAggregator() { + return activeGrouping!=null && aggregatorGrouping!=null && aggregatorGrouping.equals(activeGrouping.getName()); + } + + public boolean isNode() { + return ! isAggregator(); + } + /*private static class StreamGobbler implements Runnable { private InputStream inputStream1; private InputStream inputStream2; @@ -1130,6 +1282,19 @@ public class CommandExecutor { 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"); @@ -1151,6 +1316,22 @@ public class CommandExecutor { @Override public void statusChanged(NODE_STATUS oldStatus, 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()==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 diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Sshc.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Sshc.java index 236c95ccfae647575ecd66226c9928ea42847bb4..3ed61686a4b5eeed297398f64c6182b760227124 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Sshc.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/Sshc.java @@ -10,6 +10,8 @@ package eu.melodic.event.baguette.client; import eu.melodic.event.brokercep.BrokerCepService; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.sshd.client.ClientFactoryManager; @@ -42,8 +44,8 @@ import java.util.Optional; /** * Custom SSH client */ -@Service @Slf4j +@Service public class Sshc { private BaguetteClientProperties config; private SshClient client; @@ -56,16 +58,23 @@ public class Sshc { @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; + public void setConfiguration(BaguetteClientProperties config) throws IOException { this.config = config; this.clientId = config.getClientId(); log.trace("Sshc: cmd-exec: {}", commandExecutor); - this.commandExecutor.setConfiguration(config); + if (this.commandExecutor!=null) this.commandExecutor.setConfiguration(config); } public synchronized void start(boolean retry) throws IOException { @@ -112,46 +121,47 @@ public class Sshc { //client.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE); //client.setServerKeyVerifier(new RequiredServerKeyVerifier(....)); - client.setServerKeyVerifier(new ServerKeyVerifier() - { - private String serverFingerprint; - private String serverPubKey; - - public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) { - - // Print server address info - log.info("verifyServerKey(): remoteAddress: {}", remoteAddress.toString()); - - // Check that server public key fingerprint matches with the one in configuration - String fingerprint = KeyUtils.getFingerPrint(serverKey); - log.info("verifyServerKey(): serverKey: fingerprint: {}", fingerprint); - //if ( fingerprint!=null && KeyUtils.checkFingerPrint(serverFingerprint, serverKey).getFirst() ) log.info("verifyServerKey(): serverKey: fingerprint: MATCH"); - //else log.warn("verifyServerKey(): serverKey: fingerprint: NO MATCH"); - - // Check that server public key matches with the one in configuration - try { - log.debug("verifyServerKey(): serverKey: decoder: {}", KeyUtils.getPublicKeyEntryDecoder(serverKey).getClass()); - java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); - ((RSAPublicKeyDecoder) KeyUtils.getPublicKeyEntryDecoder(serverKey)).encodePublicKey(baos, (RSAPublicKey) serverKey); - String keyStr = new String(Base64.getEncoder().encode(baos.toByteArray())); - log.debug("verifyServerKey(): serverKey: server public key: \n{}", keyStr); - - return keyStr.equalsIgnoreCase(serverPubKey); - - } catch (Exception ex) { - log.error("verifyServerKey(): serverKey: EXCEPTION: ", ex); - return false; + if (useServerKeyVerifier) { + client.setServerKeyVerifier(new ServerKeyVerifier() { + private String serverFingerprint; + private String serverPubKey; + + public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) { + + // Print server address info + log.info("verifyServerKey(): remoteAddress: {}", remoteAddress.toString()); + + // Check that server public key fingerprint matches with the one in configuration + String fingerprint = KeyUtils.getFingerPrint(serverKey); + log.info("verifyServerKey(): serverKey: fingerprint: {}", fingerprint); + //if ( fingerprint!=null && KeyUtils.checkFingerPrint(serverFingerprint, serverKey).getFirst() ) log.info("verifyServerKey(): serverKey: fingerprint: MATCH"); + //else log.warn("verifyServerKey(): serverKey: fingerprint: NO MATCH"); + + // Check that server public key matches with the one in configuration + try { + log.debug("verifyServerKey(): serverKey: decoder: {}", KeyUtils.getPublicKeyEntryDecoder(serverKey).getClass()); + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + ((RSAPublicKeyDecoder) KeyUtils.getPublicKeyEntryDecoder(serverKey)).encodePublicKey(baos, (RSAPublicKey) serverKey); + String keyStr = new String(Base64.getEncoder().encode(baos.toByteArray())); + log.debug("verifyServerKey(): serverKey: server public key: \n{}", keyStr); + + return keyStr.equalsIgnoreCase(serverPubKey); + + } catch (Exception ex) { + log.error("verifyServerKey(): serverKey: EXCEPTION: ", ex); + return false; + } } - } - public ServerKeyVerifier setServerPubKey(String pubkey, String fingerprint) { - this.serverFingerprint = fingerprint; - this.serverPubKey = pubkey; - return this; + public ServerKeyVerifier setServerPubKey(String pubkey, String fingerprint) { + this.serverFingerprint = fingerprint; + this.serverPubKey = pubkey; + return this; + } } - } - .setServerPubKey(serverPubKey, serverFingerprint) - ); + .setServerPubKey(serverPubKey, serverFingerprint) + ); + } this.simple = SshClient.wrapAsSimpleClient(client); //simple.setConnectTimeout(...CONNECT_TIMEOUT...); @@ -204,17 +214,8 @@ public class Sshc { log.info("SSH client stopped"); } - public void run() throws IOException { + public synchronized void greeting() { if (!started) return; - - // Start communication protocol with Server - // Execution waits here until connection is closed - log.trace("run(): Calling communicateWithServer()..."); - communicateWithServer(in, out, out); - } - - protected void communicateWithServer(InputStream in, PrintStream out, PrintStream err) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String certOneLine = Optional .ofNullable(brokerCepService.getBrokerCertificate()) .orElse("") @@ -232,21 +233,15 @@ public class Sshc { brokerCepService.getBrokerPassword(), certOneLine); out.flush(); - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - log.info(line); - try { - boolean exit = commandExecutor.execCmd(line.split("[ \t]+"), in, out, err); - if (exit) break; - } catch (Exception ex) { - log.error("", ex); - // Report exception back to server - err.println(ex); - ex.printStackTrace(err); - err.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/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/BrokerUtil.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/BrokerUtil.java index 3b1041e7fed8d9c0e9c2b228613a2399cf603a9b..144548a4d6156f7bf2c65fd5015016a3eef74f10 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/BrokerUtil.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/BrokerUtil.java @@ -9,6 +9,7 @@ package eu.melodic.event.baguette.client.cluster; +import io.atomix.cluster.ClusterMembershipEvent; import io.atomix.cluster.Member; import io.atomix.core.Atomix; import lombok.Getter; @@ -397,17 +398,39 @@ public class BrokerUtil extends AbstractLogBase { // 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)) + .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/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManager.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManager.java index 9958b2744e127613a0bf2881772fcd8f999839cf..6080184df81d2aa77d763bb012189c6d1f4295f3 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManager.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManager.java @@ -25,15 +25,20 @@ 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 { @@ -54,6 +59,11 @@ public class ClusterManager extends AbstractLogBase { @Setter(AccessLevel.NONE) private BrokerUtil brokerUtil = null; + @Autowired + private TaskScheduler taskScheduler; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private ScheduledFuture checkerTask; + // ------------------------------------------------------------------------ public synchronized ClusterCLI getCli() { @@ -184,6 +194,8 @@ public class ClusterManager extends AbstractLogBase { log_info("CLM: {}: node={}", event.type(), event.subject().id().id()); brokerUtil.checkBroker(); } + if (callback!=null) + callback.clusterChanged(event); } }); @@ -197,6 +209,20 @@ public class ClusterManager extends AbstractLogBase { 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?)"); + }, delay); + } else { + log_warn("CLM: Cluster checker is DISABLED"); + } } public void waitToJoin() { @@ -204,6 +230,8 @@ public class ClusterManager extends AbstractLogBase { if (isInitialized() && isRunning()) break; try { Thread.sleep(500); } catch (InterruptedException e) { break; } } + if (callback!=null) + callback.joinedCluster(); } public void waitToJoin(long waitForMillis) { @@ -214,9 +242,18 @@ public class ClusterManager extends AbstractLogBase { 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(); @@ -226,6 +263,8 @@ public class ClusterManager extends AbstractLogBase { log_debug("CLM: Left cluster in {}ms", endTm-startTm); atomix = null; brokerUtil = null; + if (callback!=null) + callback.leftCluster(); } // ------------------------------------------------------------------------ diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManagerProperties.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManagerProperties.java index 3c9972274df7fd5091097e0c1745bb1d12aafe2f..f1915f6cea799e7941c6ceea6982124560d24bda 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManagerProperties.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/ClusterManagerProperties.java @@ -38,6 +38,9 @@ public class ClusterManagerProperties { 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"; diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/TestCallback.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/TestCallback.java index 90063cdc86613db405b2aba93ea1561d8f455a5e..016f363feae779836b0ec7a25f926890526e2ada 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/TestCallback.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/cluster/TestCallback.java @@ -9,6 +9,7 @@ package eu.melodic.event.baguette.client.cluster; +import io.atomix.cluster.ClusterMembershipEvent; import io.atomix.cluster.Member; import io.atomix.utils.net.Address; @@ -20,6 +21,9 @@ public class TestCallback extends AbstractLogBase implements BrokerUtil.NodeCall address = localAddress.toString(); } + public void joinedCluster() { } + public void leftCluster() { } + public void initialize() { if ("L2".equals(state)) { log_warn("__TestNode at {}: Already initialized: {}", address, state); @@ -66,6 +70,10 @@ public class TestCallback extends AbstractLogBase implements BrokerUtil.NodeCall 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()); } diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/ClientCollectorContext.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/ClientCollectorContext.java new file mode 100644 index 0000000000000000000000000000000000000000..6a92a4b105d2191b30ec225e05c90acd67f20e7e --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/ClientCollectorContext.java @@ -0,0 +1,57 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.collector; + +import eu.melodic.event.baguette.client.CommandExecutor; +import eu.melodic.event.brokercep.event.EventMap; +import eu.melodic.event.common.collector.CollectorContext; +import eu.melodic.event.util.ClientConfiguration; +import eu.melodic.event.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 boolean sendEvent(String connectionString, String destinationName, EventMap event, boolean createDestination) { + return commandExecutor.sendEvent(connectionString, destinationName, event, createDestination); + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/netdata/NetdataCollector.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/netdata/NetdataCollector.java index 6471af590e33d9c4184761b1257745b5b11c5ffb..f8eb53e5fab7c4071f613f6ff3b65f463988b974 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/netdata/NetdataCollector.java +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/netdata/NetdataCollector.java @@ -10,59 +10,43 @@ package eu.melodic.event.baguette.client.collector.netdata; import eu.melodic.event.baguette.client.Collector; -import eu.melodic.event.baguette.client.CommandExecutor; -import eu.melodic.event.brokercep.event.EventMap; +import eu.melodic.event.baguette.client.collector.ClientCollectorContext; +import eu.melodic.event.common.collector.CollectorContext; +import eu.melodic.event.common.collector.netdata.NetdataCollectorProperties; +import eu.melodic.event.util.EventBus; import eu.melodic.event.util.GROUPING; import eu.melodic.event.util.GroupingConfiguration; -import lombok.RequiredArgsConstructor; +import lombok.NonNull; 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 org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import java.util.*; +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 server + * Collects measurements from Netdata http server */ @Slf4j @Component -@RequiredArgsConstructor -public class NetdataCollector implements Collector, InitializingBean, Runnable { - private final NetdataCollectorProperties properties; - private final CommandExecutor commandExecutor; - - private RestTemplate restTemplate = new RestTemplate(); - private boolean started; - private Thread runner; - private boolean running; - private List allowedTopics; - private Map topicMap; - - @Override - public void afterPropertiesSet() { - log.debug("Collectors::Netdata: properties: {}", 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]: "")); - +public class NetdataCollector extends eu.melodic.event.common.collector.netdata.NetdataCollector implements Collector { + public NetdataCollector(@NonNull NetdataCollectorProperties properties, + @NonNull CollectorContext collectorContext, + @NonNull TaskScheduler taskScheduler, + @NonNull EventBus eventBus) + { + super(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 = commandExecutor.getGroupings().get(g); + GroupingConfiguration grp = ((ClientCollectorContext)collectorContext).getGroupings().get(g); if (grp!=null) topics.addAll(grp.getEventTypeNames()); } @@ -81,131 +65,4 @@ public class NetdataCollector implements Collector, InitializingBean, Runnable { } } - public synchronized void start() { - // check if already running - if (started) { - log.warn("Collectors::Netdata: Already started"); - return; - } - - // check parameters - if (properties==null || !properties.isEnable()) { - log.warn("Collectors::Netdata: Collector not enabled"); - return; - } - if (properties.getDelay()<0) properties.setDelay(0); - 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); - } - - log.info("Collectors::Netdata: configuration: {}", properties); - - // start thread - runner = new Thread(this, "baguette-client-collector-netdata-thread"); - runner.setDaemon(true); - started = true; - running = true; - runner.start(); - - log.info("Collectors::Netdata: Started"); - } - - public synchronized void stop() { - if (!started) { - log.warn("Collectors::Netdata: Not started"); - return; - } - running = false; - // interrupt sleep - runner.interrupt(); - } - - public void run() { - if (!started) return; - - while (running && !Thread.currentThread().isInterrupted()) { - try { - // collect data - collectAndPublishData(); - - // sleep for 'delay' millis - Thread.sleep(properties.getDelay()); - } catch (InterruptedException e) { - log.warn("Collectors::Netdata: Interrupted"); - } catch (Throwable t) { - log.warn("Collectors::Netdata: Exception: {}", t); - } - } - - synchronized (this) { - log.info("Collectors::Netdata: Stopped"); - started = false; - running = false; - } - } - - private void collectAndPublishData() { - log.info("Collectors::Netdata: Collecting data: {}...", properties.getUrl()); - long startTm = System.currentTimeMillis(); - ResponseEntity response = restTemplate.getForEntity(properties.getUrl(), HashMap.class); - long callEndTm = System.currentTimeMillis(); - log.trace("Collectors::Netdata: ...response: {}", response); - if (response.getStatusCode()==HttpStatus.OK) { - Map dataMap = response.getBody(); - boolean createTopic = properties.isCreateTopic(); - int countSuccess = 0; - int countErrors = 0; - log.trace("Collectors::Netdata: ...keys: {}", dataMap.keySet()); - 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.toString()+"."+dimKey.toString()).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); - try { - boolean createDestination = (createTopic || allowedTopics!=null && allowedTopics.contains(metricName)); - if (topicMap!=null) { - String targetTopic = topicMap.get(metricName); - if (targetTopic!=null && !targetTopic.isEmpty()) - metricName = targetTopic; - } - EventMap event = new EventMap(metricValue, 1, timestamp); - log.debug("Collectors::Netdata: {}: {}", metricName, metricValue); - if (commandExecutor.sendEvent(null, metricName, event, createDestination)) - countSuccess++; - } catch (Exception e) { - log.warn("Collectors::Netdata: Publishing netdata metric failed: ", e); - countErrors++; - } - } - } - - if (Thread.currentThread().isInterrupted()) break; - } - long endTm = System.currentTimeMillis(); - log.info("Collectors::Netdata: Collecting data...ok"); - log.info("Collectors::Netdata: Metrics: extracted={}, published={}, failed={}", - countSuccess+countErrors, countSuccess, countErrors); - log.info("Collectors::Netdata: Durations: rest-call={}, extract+publish={}, total={}", - callEndTm-startTm, endTm-callEndTm, endTm-startTm); - } else { - log.warn("Collectors::Netdata: Collecting data...failed: Http Status: {}", response.getStatusCode()); - } - } } diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/EmsClientRecoveryTask.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/EmsClientRecoveryTask.java new file mode 100644 index 0000000000000000000000000000000000000000..8a7cacbe763b260a5301968a937f014db05473c7 --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/EmsClientRecoveryTask.java @@ -0,0 +1,55 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import eu.melodic.event.util.EventBus; +import eu.melodic.event.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * EMS client (client-side) Self-Healing + */ +@Slf4j +@Component +public class EmsClientRecoveryTask extends VmNodeRecoveryTask { + @Getter + private final List recoveryCommands = Collections.unmodifiableList(Arrays.asList( + new RECOVERY_COMMAND("Initial wait...", + "pwd",0, 10000), + new RECOVERY_COMMAND("Sending baguette client kill command...", + "/opt/baguette-client/bin/kill.sh",0, 2000), + new RECOVERY_COMMAND("Sending baguette client start command...", + "/opt/baguette-client/bin/run.sh",0, 10000) + )); + + @Value("${self.healing.recovery.file.baguette:}") + private String emsRecoveryFile; + + public EmsClientRecoveryTask(@NonNull EventBus eventBus, @NonNull PasswordUtil passwordUtil, @NonNull TaskScheduler taskScheduler) { + super(eventBus, passwordUtil, taskScheduler); + } + + public void runNodeRecovery() throws Exception { + if (StringUtils.isNotBlank(emsRecoveryFile)) + runNodeRecovery(emsRecoveryFile); + else + runNodeRecovery(recoveryCommands); + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NetdataAgentLocalRecoveryTask.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NetdataAgentLocalRecoveryTask.java new file mode 100644 index 0000000000000000000000000000000000000000..5ef1e707df689fc0dd917544baaae211c494ff2d --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NetdataAgentLocalRecoveryTask.java @@ -0,0 +1,55 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import eu.melodic.event.util.EventBus; +import eu.melodic.event.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Netdata agent (client-side) 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, 5000), + 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) + )); + + @Value("${self.healing.recovery.file.netdata:}") + private String netdataRecoveryFile; + + public NetdataAgentLocalRecoveryTask(@NonNull EventBus eventBus, @NonNull PasswordUtil passwordUtil, @NonNull TaskScheduler taskScheduler) { + super(eventBus, taskScheduler); + } + + public void runNodeRecovery() throws Exception { + if (StringUtils.isNotBlank(netdataRecoveryFile)) + runNodeRecovery(netdataRecoveryFile); + else + runNodeRecovery(recoveryCommands); + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NetdataAgentRecoveryTask.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NetdataAgentRecoveryTask.java new file mode 100644 index 0000000000000000000000000000000000000000..1b986d6c56c99687e132d50b4fc163476753de11 --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NetdataAgentRecoveryTask.java @@ -0,0 +1,55 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import eu.melodic.event.util.EventBus; +import eu.melodic.event.util.PasswordUtil; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Netdata agent (client-side) Self-Healing + */ +@Slf4j +@Component +public class NetdataAgentRecoveryTask extends VmNodeRecoveryTask { + @Getter + private final List recoveryCommands = Collections.unmodifiableList(Arrays.asList( + new RECOVERY_COMMAND("Initial wait...", + "pwd",0, 5000), + 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) + )); + + @Value("${self.healing.recovery.file.netdata:}") + private String netdataRecoveryFile; + + public NetdataAgentRecoveryTask(@NonNull EventBus eventBus, @NonNull PasswordUtil passwordUtil, @NonNull TaskScheduler taskScheduler) { + super(eventBus, passwordUtil, taskScheduler); + } + + public void runNodeRecovery() throws Exception { + if (StringUtils.isNotBlank(netdataRecoveryFile)) + runNodeRecovery(netdataRecoveryFile); + else + runNodeRecovery(recoveryCommands); + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NodeInfoHelper.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NodeInfoHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..dd7071c8b0a231205c54148d11ce90af6ee056b9 --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/NodeInfoHelper.java @@ -0,0 +1,74 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import com.google.gson.Gson; +import eu.melodic.event.baguette.client.BaguetteClientProperties; +import eu.melodic.event.baguette.client.CommandExecutor; +import eu.melodic.event.util.EventBus; +import eu.melodic.event.util.PasswordUtil; +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; + +/** + * 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={}, node-info={}", nodeId, nodeAddress, nodeInfo); + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/RECOVERY_COMMAND.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/RECOVERY_COMMAND.java new file mode 100644 index 0000000000000000000000000000000000000000..727499505c8968e29b2a7b0d3f42cb4b84fd739f --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/RECOVERY_COMMAND.java @@ -0,0 +1,20 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import lombok.Data; + +@Data +class RECOVERY_COMMAND { + private final String name; + private final String command; + private final long waitBefore; + private final long waitAfter; +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/RecoveryTask.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/RecoveryTask.java new file mode 100644 index 0000000000000000000000000000000000000000..87e5dc1cb0da1235262f0a20efbf09869a5ecbbb --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/RecoveryTask.java @@ -0,0 +1,41 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.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; + +/** + * Client-side Self-Healing task + */ +public interface RecoveryTask { + Map getNodeInfo(); + void setNodeInfo(Map nodeInfo); + + List getRecoveryCommands(); + + void runNodeRecovery() throws Exception; + + void runNodeRecovery(List recoveryCommandsList) throws Exception; + + default void runNodeRecovery(String recoveryCommandsFile) 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); + } + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/SelfHealingPlugin.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/SelfHealingPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..33be7c8fe12bd951e16edd4bf43d65f0aa78a7ab --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/SelfHealingPlugin.java @@ -0,0 +1,302 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import eu.melodic.event.baguette.client.BaguetteClientProperties; +import eu.melodic.event.baguette.client.CommandExecutor; +import eu.melodic.event.baguette.client.collector.netdata.NetdataCollector; +import eu.melodic.event.util.EventBus; +import eu.melodic.event.util.PasswordUtil; +import eu.melodic.event.util.Plugin; +import io.atomix.cluster.ClusterMembershipEvent; +import lombok.NonNull; +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.beans.factory.annotation.Value; +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 CommandExecutor commandExecutor; + private final EventBus eventBus; + private final PasswordUtil passwordUtil; + private final NodeInfoHelper nodeInfoHelper; + + final static String SELF_HEALING_RECOVERY_FAILED = "SELF_HEALING_RECOVERY_FAILED"; + final static String SELF_HEALING_RECOVERY_COMPLETED = "SELF_HEALING_RECOVERY_COMPLETED"; + + private boolean started; + + private final HashMap> waitingTasks = new HashMap<>(); + private final TaskScheduler taskScheduler; + + @Value("${self.healing.enabled:true}") + private boolean enabled; + @Value("${self.healing.recovery.delay:10000}") + private long clientRecoveryDelay; + @Value("${self.healing.recovery.retry.wait:60000}") + private long clientRecoveryRetryDelay; + @Value("${self.healing.recovery.max.retries:3}") + private int clientRecoveryMaxRetries; + + @Override + public void afterPropertiesSet() { + log.debug("SelfHealingPlugin: properties: {}", properties); + } + + 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_PAUSED, this); + eventBus.subscribe(NetdataCollector.NETDATA_NODE_RESUMED, 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_PAUSED, this); + eventBus.unsubscribe(NetdataCollector.NETDATA_NODE_RESUMED, this); + + // Cancel all waiting recovery tasks + waitingTasks.forEach((nodeAddress,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 (!enabled) 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_PAUSED.equals(topic)) { + log.debug("SelfHealingPlugin: onMessage(): NETDATA NODE PAUSED: message={}", message); + processNetdataNodePausedEvent(message); + } else + if (NetdataCollector.NETDATA_NODE_RESUMED.equals(topic)) { + log.debug("SelfHealingPlugin: onMessage(): NETDATA NODE RESUMED: message={}", message); + processNetdataNodeResumedEvent(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, 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, false); + } else { + log.warn("SelfHealingPlugin: processClusterNodeAddedEvent(): Message is not a {} object. Will ignore it.", ClusterMembershipEvent.class.getSimpleName()); + } + } + + // ------------------------------------------------------------------------ + + private void processNetdataNodePausedEvent(Object message) { + log.debug("SelfHealingPlugin: processNetdataNodePausedEvent(): BEGIN: message={}", message); + if (!(message instanceof Map)) { + log.warn("SelfHealingPlugin: processNetdataNodePausedEvent(): Message is not a {} object. Will ignore it.", Map.class.getSimpleName()); + return; + } + + // Get paused node address + Object addressValue = ((Map) message).getOrDefault("address", null); + log.debug("SelfHealingPlugin: processNetdataNodePausedEvent(): node-address={}", addressValue); + if (addressValue==null) { + log.warn("SelfHealingPlugin: processNetdataNodePausedEvent(): 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, "", NetdataAgentLocalRecoveryTask.class); + } else { + // Aggregator is responsible for recovering remote Netdata agents + createRecoveryTask(null, nodeAddress, 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 processNetdataNodeResumedEvent(Object message) { + log.debug("SelfHealingPlugin: processNetdataNodeResumedEvent(): BEGIN: message={}", message); + if (!(message instanceof Map)) { + log.warn("SelfHealingPlugin: processNetdataNodeResumedEvent(): Message is not a {} object. Will ignore it.", Map.class.getSimpleName()); + return; + } + + // Get resumed node address + String nodeAddress = ((Map) message).getOrDefault("address", "").toString(); + log.debug("SelfHealingPlugin: processNetdataNodeResumedEvent(): node-address={}", nodeAddress); + /*if (StringUtils.isBlank(nodeAddress)) { + log.warn("SelfHealingPlugin: processNetdataNodeResumedEvent(): Node address is missing. Initial message: {}", message); + return; + }*/ + + // Cancel any waiting recovery task + cancelRecoveryTask(null, nodeAddress, false); + } + + // ------------------------------------------------------------------------ + + private void createRecoveryTask(String nodeId, @NonNull String nodeAddress, @NonNull Class recoveryTaskClass) { + // Check if a recovery task has already been scheduled + synchronized (waitingTasks) { + if (waitingTasks.containsKey(nodeAddress)) { + log.warn("SelfHealingPlugin: createRecoveryTask(): Recovery has already been scheduled for Node: id={}, address={}", nodeId, nodeAddress); + return; + } + waitingTasks.put(nodeAddress, 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.size() == 0) { + 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={}, node-info:\n{}", nodeId, nodeAddress, nodeInfo); + } 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.size()>0) + recoveryTask.setNodeInfo(nodeInfo); + AtomicInteger retries = new AtomicInteger(0); + ScheduledFuture future = taskScheduler.scheduleWithFixedDelay(() -> { + try { + log.info("SelfHealingPlugin: Retry #{}: Recovering node: id={}, address={}", retries.get(), nodeId, nodeAddress); + recoveryTask.runNodeRecovery(); + //NOTE: 'recoveryTask.runNodeRecovery()' must send SELF_HEALING_RECOVERY_COMPLETED or _FAILED event + if (retries.getAndIncrement() > clientRecoveryMaxRetries) { + log.warn("SelfHealingPlugin: Max retries reached. No more recovery retries for node: id={}, address={}", nodeId, nodeAddress); + cancelRecoveryTask(nodeId, nodeAddress, true); + } + } catch (Exception e) { + log.error("SelfHealingPlugin: EXCEPTION while recovering node: node-info={} -- Exception: ", recoveryTask.getNodeInfo(), e); + eventBus.send(SELF_HEALING_RECOVERY_FAILED, nodeAddress); + } + }, Instant.now().plusMillis(clientRecoveryDelay), Duration.ofMillis(clientRecoveryRetryDelay)); + waitingTasks.put(nodeAddress, future); + log.info("SelfHealingPlugin: createRecoveryTask(): Created recovery task for Node: id={}, address={}", nodeId, nodeAddress); + } + + private void cancelRecoveryTask(String nodeId, @NonNull String nodeAddress, boolean retainAddress) { + synchronized (waitingTasks) { + ScheduledFuture future = retainAddress ? waitingTasks.put(nodeAddress, null) : waitingTasks.remove(nodeAddress); + 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.warn("SelfHealingPlugin: cancelRecoveryTask(): No recovery task is scheduled for Node: id={}, address={}", nodeId, nodeAddress); + } + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/ShellRecoveryTask.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/ShellRecoveryTask.java new file mode 100644 index 0000000000000000000000000000000000000000..05cae3af2b992d3bc6dc53fc9eb3e15685649ad6 --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/ShellRecoveryTask.java @@ -0,0 +1,113 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import eu.melodic.event.util.EventBus; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +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.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static eu.melodic.event.baguette.client.plugin.recovery.SelfHealingPlugin.SELF_HEALING_RECOVERY_COMPLETED; + +/** + * Client-side, Local-node Self-Healing + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ShellRecoveryTask implements RecoveryTask { + @NonNull private final EventBus eventBus; + @NonNull private final TaskScheduler taskScheduler; + + @Getter @Setter + private Map nodeInfo; + + public void setNodeInfo(@NonNull Map nodeInfo) { + this.nodeInfo = nodeInfo; + } + + @SneakyThrows + public List getRecoveryCommands() { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery() throws Exception { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery(List recoveryCommands) throws Exception { + log.debug("ShellRecoveryTask: runNodeRecovery(): node-info={}", nodeInfo); + + // 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 + log.warn("############## {}...", command.getName()); + Process process = Runtime.getRuntime().exec(command.getCommand()); + + // 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 waitFor(long millis, String description) { + if (millis>0) { + log.warn("############## Waiting for {}ms after {}...", millis, description); + try { Thread.sleep(millis); } catch (InterruptedException e) { } + } + } + + private void redirectShellOutput(InputStream in, String id, AtomicBoolean closed) { + taskScheduler.schedule(() -> { + try { + //IoUtils.copy(in, System.out); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + while (reader.ready()) { + log.info(" {}> {}", id, reader.readLine()); + } + } + } catch (IOException e) { + if (closed.get()) { + log.info("ShellRecoveryTask: redirectShellOutput(): Connection closed: id={}", id); + } else { + log.error("ShellRecoveryTask: redirectShellOutput(): Exception while copying Process IN stream: id={}\n", id, e); + } + } + }, + Instant.now() + ); + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/VmNodeRecoveryTask.java b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/VmNodeRecoveryTask.java new file mode 100644 index 0000000000000000000000000000000000000000..4ab05b5ca3dd5632881e087cec679995c51049d9 --- /dev/null +++ b/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/plugin/recovery/VmNodeRecoveryTask.java @@ -0,0 +1,189 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.client.plugin.recovery; + +import eu.melodic.event.baguette.client.BaguetteClientProperties; +import eu.melodic.event.baguette.client.Sshc; +import eu.melodic.event.util.EventBus; +import eu.melodic.event.util.PasswordUtil; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +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.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static eu.melodic.event.baguette.client.plugin.recovery.SelfHealingPlugin.SELF_HEALING_RECOVERY_COMPLETED; + +/** + * Client-side, VM-node Self-Healing + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class VmNodeRecoveryTask implements RecoveryTask { + @NonNull private final EventBus eventBus; + @NonNull private final PasswordUtil passwordUtil; + @NonNull private final TaskScheduler taskScheduler; + + @Getter @Setter + private Map nodeInfo; + + private BaguetteClientProperties baguetteClientProperties; + + public void setNodeInfo(@NonNull Map nodeInfo) { + this.nodeInfo = nodeInfo; + this.baguetteClientProperties = createBaguetteClientProperties(); + } + + @SneakyThrows + public List getRecoveryCommands() { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery() throws Exception { + throw new Exception("Method not implemented. Use 'runNodeRecovery(List)' instead"); + } + + public void runNodeRecovery(List recoveryCommands) throws Exception { + log.debug("VmNodeRecoveryTask: runNodeRecovery(): node-info={}", nodeInfo); + + // Connect to Node (VM) + Sshc 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()); + log.warn("############## {}...", command.getName()); + sshc.getOut().println(command.getCommand()); + 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, baguetteClientProperties.getServerAddress()); + } + + private String str(Object o) { + if (o==null) return ""; + return o.toString(); + } + + private void waitFor(long millis, String description) { + if (millis>0) { + log.warn("############## Waiting for {}ms after {}...", millis, description); + try { Thread.sleep(millis); } catch (InterruptedException e) { } + } + } + + private BaguetteClientProperties createBaguetteClientProperties() { + log.debug("VmNodeRecoveryTask: createBaguetteClientProperties(): node-info={}", nodeInfo); + + // 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")); + int port = 22; + try { + if (StringUtils.isNotBlank(portStr)) + port = Integer.parseInt(portStr); + if (port<1 || port>65535) + port = 22; + } catch (Exception e) {} + + log.debug("VmNodeRecoveryTask: createBaguetteClientProperties(): os={}, address={}, type={}", os, address, type); + log.debug("VmNodeRecoveryTask: createBaguetteClientProperties(): username={}, password={}", username, passwordUtil.encodePassword(password)); + log.debug("VmNodeRecoveryTask: createBaguetteClientProperties(): fingerprint={}, key={}", fingerprint, passwordUtil.encodePassword(key)); + + // Connect to node and restart Baguette Client + BaguetteClientProperties config = new BaguetteClientProperties(); + config.setServerAddress(address); + config.setServerPort(port); + config.setServerUsername(username); + if (!password.isEmpty()) { + config.setServerPassword(password); + } + if (!key.isEmpty()) { + config.setServerPubkey(key); + config.setServerFingerprint(fingerprint); + } + + //XXX:TODO: Make recovery authTimeout configurable + config.setAuthTimeout(60000); + + return config; + } + + private Sshc connectToNode() throws IOException { + Sshc sshc = new Sshc(); + sshc.setConfiguration(baguetteClientProperties); + //XXX:TODO: Try enabling server key verification + sshc.setUseServerKeyVerifier(false); + log.info("VmNodeRecoveryTask: connectToNode(): Connecting to node using SSH: address={}, port={}, username={}", + baguetteClientProperties.getServerAddress(), baguetteClientProperties.getServerPort(), baguetteClientProperties.getServerUsername()); + sshc.start(); + log.debug("VmNodeRecoveryTask: connectToNode(): Connected to node: address={}, port={}, username={}", + baguetteClientProperties.getServerAddress(), baguetteClientProperties.getServerPort(), baguetteClientProperties.getServerUsername()); + return sshc; + } + + private void disconnectFromNode(Sshc sshc, AtomicBoolean closed) throws IOException { + log.info("VmNodeRecoveryTask: disconnectFromNode(): Disconnecting from node: address={}, port={}, username={}", + baguetteClientProperties.getServerAddress(), baguetteClientProperties.getServerPort(), baguetteClientProperties.getServerUsername()); + closed.set(true); + sshc.stop(); + log.debug("VmNodeRecoveryTask: disconnectFromNode(): Disconnected from node: address={}, port={}, username={}", + baguetteClientProperties.getServerAddress(), baguetteClientProperties.getServerPort(), baguetteClientProperties.getServerUsername()); + } + + private void redirectSshOutput(InputStream in, String id, AtomicBoolean closed) { + taskScheduler.schedule(() -> { + try { + //IoUtils.copy(sshc.getIn(), System.out); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + while (reader.ready()) { + log.info(" {}> {}", id, reader.readLine()); + } + } + } catch (IOException e) { + if (closed.get()) { + log.info("VmNodeRecoveryTask: redirectSshOutput(): Connection closed: id={}", id); + } else { + log.error("VmNodeRecoveryTask: redirectSshOutput(): Exception while copying SSH IN stream: id={}\n", id, e); + } + } + }, + Instant.now() + ); + } +} diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/BaguetteServer.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/BaguetteServer.java index b96b75e878eafa8b7af6ffdf551ffb9b72b640da..28cee7c6675ce66364c56cc7a48e296bf7627cb6 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/BaguetteServer.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/BaguetteServer.java @@ -12,30 +12,37 @@ package eu.melodic.event.baguette.server; import eu.melodic.event.baguette.server.properties.BaguetteServerProperties; import eu.melodic.event.brokercep.BrokerCepService; import eu.melodic.event.translate.TranslationContext; -import eu.melodic.event.util.FunctionDefinition; -import eu.melodic.event.util.PasswordUtil; +import eu.melodic.event.util.*; 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.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.*; +import java.util.stream.Collectors; /** * Baguette Server */ @Slf4j @Service -public class BaguetteServer { +public class BaguetteServer implements InitializingBean { @Autowired private BaguetteServerProperties config; @Autowired private PasswordUtil passwordUtil; @Autowired private NodeRegistry nodeRegistry; + @Autowired + private EventBus eventBus; private Sshd server; @@ -48,17 +55,56 @@ public class BaguetteServer { 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.afterPropertiesSet(): Generated new Baguette Server username/password: username={}, password={}", genUsername, + credentials.getPasswordEncoder()!=null ? credentials.getPasswordEncoder().encode(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) - groupings.remove(upperwareGrouping); + 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.size()>0 ? list.get(0) : null; + } + public BaguetteServerProperties getConfiguration() { return config; } @@ -105,7 +151,8 @@ public class BaguetteServer { log.info("BaguetteServer.startServer(): Starting SSH server instance..."); nodeRegistry.setCoordinator(coordinator); Sshd server = new Sshd(); - server.start(config, coordinator); + server.start(config, coordinator, eventBus, nodeRegistry); + server.setNodeRegistry(getNodeRegistry()); this.server = server; log.info("BaguetteServer.startServer(): Starting SSH server instance... done"); } else { @@ -264,24 +311,155 @@ public class BaguetteServer { server.sendToClient(clientId, command); } - public Object readFromClient(String clientId, String command) { - return server.readFromClient(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 server.getActiveClients(); + return ClientShellCommand.getActive().stream() + .map(c -> { + NodeRegistryEntry entry = getNodeRegistryEntryFromClientShellCommand(c); + return formatClientList(c, entry); + }) + .sorted() + .collect(Collectors.toList()); } public Map> getActiveClientsMap() { - return server.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))); + } + + 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) { + 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.isBlank(hostname)) { + try { + hostname = InetAddress.getByName(address).getHostName(); + } catch (Exception e) { + log.warn("Failed to resolve client hostname from IP address: {}\n", address, e); + } + } + if (StringUtils.isNotBlank(hostname)) { + if (c!=null) c.setClientClusterNodeHostname(hostname); + if (entry!=null) entry.setHostname(hostname); + } + } + 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); + return properties; } public void sendConstants(Map constants) { server.sendConstants(constants); } - //XXX: TODO: do actual node registration with Server coordinator. More information might be needed or returned. - public String registerClient(Map nodeInfoMap) { + public NodeRegistryEntry registerClient(Map nodeInfoMap) throws UnknownHostException { log.debug("BaguetteServer.registerClient(): node-info={}", nodeInfoMap); Map nodeInfo = new HashMap<>(nodeInfoMap); @@ -302,9 +480,6 @@ public class BaguetteServer { log.debug("BaguetteServer.registerClient(): client-id={}", clientId); // Add node info into node registry - nodeInfo.put("baguette-client-id", clientId); - nodeRegistry.addNode(nodeInfo); - - return clientId; + return nodeRegistry.addNode(nodeInfo, clientId); } } diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ClientShellCommand.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ClientShellCommand.java index 34368618d0d65365bd8007bd957e78a8ba958052..7d26f99b5a09d8c48d4d39850860a38ca1db301e 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ClientShellCommand.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ClientShellCommand.java @@ -9,8 +9,13 @@ package eu.melodic.event.baguette.server; +import com.google.gson.Gson; +import eu.melodic.event.util.ClientConfiguration; +import eu.melodic.event.util.EventBus; +import eu.melodic.event.baguette.server.coordinator.cluster.IClusterZone; import eu.melodic.event.util.GroupingConfiguration; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -20,6 +25,7 @@ import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.SessionAware; import org.apache.sshd.server.session.ServerSession; import org.cryptacular.util.CertUtil; +import org.slf4j.event.Level; import java.io.*; import java.net.InetSocketAddress; @@ -28,6 +34,7 @@ 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, SessionAware { @@ -35,12 +42,21 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { 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(String address) { + return activeCmdMap.get(address); + } + private InputStream in; private PrintStream out; private PrintStream err; @@ -65,7 +81,10 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { @Getter @Setter private int clientClusterNodePort; @Getter @Setter private String clientClusterNodeAddress; @Getter @Setter private String clientClusterNodeHostname; - @Getter @Setter private Object clientZone; + @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; @@ -75,19 +94,29 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { private boolean closeConnection = false; private Map inputsMap = new HashMap<>(); + private EventBus eventBus; + @Getter + private Exception lastException; + @Getter + private NodeRegistry nodeRegistry; + @Getter @Setter + private NodeRegistryEntry nodeRegistryEntry; - public ClientShellCommand(ServerCoordinator coordinator, boolean allowClientOverrideItsAddress) { + 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; } 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(); @@ -123,6 +152,7 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { public void run() { 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 { @@ -136,12 +166,17 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { callback.onExit(2); } log.info("{}--> Thread stopped immediately", id); + eventBus.send("BAGUETTE_SERVER_CLIENT_SESSION_CLOSED_IMMEDIATELY", this); return; } 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); try { log.info("{}==> Thread started", id); @@ -153,7 +188,7 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { String line; while ((line = reader.readLine()) != null) { line = line.trim(); - log.info("{}--> {}", id, line); + log.debug("{}--> {}", id, line); //if (echoOn) out.printf("CLIENT (%s) : ECHO : %s\n", id, line); if (echoOn) out.printf("ECHO %s\n", line); @@ -162,32 +197,84 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { if (line.startsWith("-HELLO FROM CLIENT:")) { getClientInfoFromGreeting(line.substring("-HELLO FROM CLIENT:".length())); coordinator.register(this); + eventBus.send("BAGUETTE_SERVER_CLIENT_REGISTERED", this); } else if (line.startsWith("-INPUT:")) { String input = line.substring("-INPUT:".length()); String[] part = input.split(":",2 ); inputsMap.put(part[0].trim(), 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("-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.equalsIgnoreCase("READY")) { coordinator.clientReady(this); } else { coordinator.processClientInput(this, line); } } + eventBus.send("BAGUETTE_SERVER_CLIENT_EXITING", this); log.info("{}==> Signaling client to exit", id); out.println("EXIT"); - } catch (IOException ex) { - log.warn("{}==> EXCEPTION : {}", id, ex); + } catch (Exception ex) { + log.warn("{}==> EXCEPTION : ", id, ex); out.printf("EXCEPTION %s\n", ex); + this.lastException = ex; + eventBus.send("BAGUETTE_SERVER_CLIENT_EXCEPTION", this); } finally { synchronized (activeCmdList) { activeCmdList.remove(this); + activeCmdMap.remove(getClientIpAddress()); } log.info("{}--> Thread stops", id); coordinator.unregister(this); + eventBus.send("BAGUETTE_SERVER_CLIENT_UNREGISTERED", this); if (!callbackCalled.getAndSet(true)) { callback.onExit(0); } + eventBus.send("BAGUETTE_SERVER_CLIENT_EXITED", this); } } @@ -319,9 +406,34 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { 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; - log.info("{}==> PUSH : {}", id, msg); + switch (logLevel) { + case TRACE: log.trace("{}==> PUSH : {}", id, msg); break; + case DEBUG: log.debug("{}==> PUSH : {}", id, msg); break; + case WARN: log.warn("{}==> PUSH : {}", id, msg); break; + case ERROR: log.error("{}==> PUSH : {}", id, msg); break; + default: log.info("{}==> PUSH : {}", id, msg); + } out.println(msg); } @@ -329,17 +441,25 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { sendToClient(cmd); } + public void sendCommand(String cmd, Level logLevel) { + sendToClient(cmd, logLevel); + } + public void sendCommand(String[] cmd) { sendToClient(String.join(" ", cmd)); } - public Object readFromClient(String 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); + 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); @@ -376,7 +496,7 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { /** * Write an object to a Base64 string. */ - protected String serializeToString(Serializable o) throws IOException { + public static String serializeToString(Serializable o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); @@ -387,7 +507,7 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { /** * Read the object from Base64 string. */ - protected Object unserializeFromString(String s) throws IOException, ClassNotFoundException { + 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(); @@ -395,6 +515,36 @@ public class ClientShellCommand implements Command, Runnable, SessionAware { 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); diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistry.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistry.java index 3c4537b133e531713f2c25a1b21861f815ca84e8..f703130b3517d253b3f107ea186bf97cb9d1968e 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistry.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistry.java @@ -15,9 +15,12 @@ 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 @@ -29,9 +32,26 @@ public class NodeRegistry { @Getter @Setter private ServerCoordinator coordinator; - public synchronized void addNode(Map nodeInfo) { - String ipAddress = getIpAddressFromNodeInfo(nodeInfo); + 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 + 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; + nodeInfo.put("original-address", nodeInfo.get("address")); + nodeInfo.put("address", ipAddress); + } catch (UnknownHostException e) { + log.error("NodeRegistry.addNode(): EXCEPTION while resolving IP address from provided hostname/address: {}\n", ipAddress, e); + throw e; + } + // 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: {}", @@ -46,9 +66,12 @@ public class NodeRegistry { } } - entry = new NodeRegistryEntry(ipAddress).nodePreregistration(nodeInfo); + // Create and register node registry entry + entry = new NodeRegistryEntry(ipAddress, clientId, coordinator.getServer()).nodePreregistration(nodeInfo); + 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) { @@ -85,6 +108,18 @@ public class NodeRegistry { 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(); } @@ -92,4 +127,6 @@ public class NodeRegistry { public Collection getNodes() { return registry.values(); } + + public Collection getNodeReferences() { return registry.values().stream().map(NodeRegistryEntry::getReference).collect(Collectors.toList()); } } diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistryEntry.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistryEntry.java index 2b649212b66ae6796070ba52d18dfd5e606f3f41..b9d572d03bf9734d58345737c30d57ed88872744 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistryEntry.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/NodeRegistryEntry.java @@ -9,44 +9,108 @@ package eu.melodic.event.baguette.server; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import com.fasterxml.jackson.annotation.JsonIgnore; +import eu.melodic.event.baguette.server.coordinator.cluster.IClusterZone; +import lombok.*; +import org.apache.commons.lang3.StringUtils; +import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; +import java.util.UUID; @Data @RequiredArgsConstructor @AllArgsConstructor public class NodeRegistryEntry { - public enum STATE { PREREGISTERED, INSTALLED, REGISTERED }; - private final String ipAddress; + public enum STATE { PREREGISTERED, IGNORE_NODE, INSTALLING, NOT_INSTALLED, INSTALLED, INSTALL_ERROR, + WAITING_REGISTRATION, REGISTERED, NOT_REGISTERED, REGISTRATION_ERROR, DISCONNECTED + }; + @Getter private final String ipAddress; + @Getter private final String clientId; + @JsonIgnore + @Getter private final transient BaguetteServer baguetteServer; + @Getter private String hostname; @Getter private STATE state = null; - @Getter private Map preregistration = new LinkedHashMap<>(); - @Getter private Map installation = new LinkedHashMap<>(); - @Getter private Map registration = new LinkedHashMap<>(); + @Getter private Date stateLastUpdate; + @Getter private String reference = UUID.randomUUID().toString(); + @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; + + 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(processMap("", nodeInfo)); // preregistration.putAll((Map)processMap(nodeInfo)); - state = STATE.PREREGISTERED; + setState(STATE.PREREGISTERED); return this; } - public NodeRegistryEntry nodeInstallation(Map nodeInfo) { + public NodeRegistryEntry nodeIgnore(Object nodeInfo) { installation.clear(); - installation.putAll(processMap("", nodeInfo)); - state = STATE.INSTALLED; + 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 nodeRegistration(Map nodeInfo) { registration.clear(); registration.putAll(processMap("", nodeInfo)); - state = STATE.REGISTERED; + setState(STATE.REGISTERED); return this; } diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ServerCoordinator.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ServerCoordinator.java index c017bb71cc83a44039432ab78754b6ea6001d148..37dc67a508ef658dbb90671c4bb051af0085fd43 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ServerCoordinator.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/ServerCoordinator.java @@ -19,6 +19,8 @@ import static eu.melodic.event.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) { } @@ -35,6 +37,8 @@ public interface ServerCoordinator { default boolean allowNotPreregisteredNode(ClientShellCommand csc) { return true; } + default void preregister(NodeRegistryEntry entry) { } + void register(ClientShellCommand c); void unregister(ClientShellCommand c); diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/Sshd.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/Sshd.java index 2b9ef45b6ee96ea7f005b4fd7882baad6e615c63..30bd4c8fc40490e894e312d636f9a7b75622128b 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/Sshd.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/Sshd.java @@ -9,7 +9,11 @@ package eu.melodic.event.baguette.server; +import eu.melodic.event.baguette.server.coordinator.cluster.ClusteringCoordinator; import eu.melodic.event.baguette.server.properties.BaguetteServerProperties; +import eu.melodic.event.util.EventBus; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.sshd.common.Factory; import org.apache.sshd.common.PropertyResolverUtils; @@ -21,6 +25,7 @@ import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.session.ServerSession; +import org.slf4j.event.Level; import java.io.File; import java.io.IOException; @@ -34,7 +39,7 @@ import java.util.stream.Collectors; */ @Slf4j public class Sshd { - private ServerCoordinator coordinator; + @Getter private ServerCoordinator coordinator; private BaguetteServerProperties configuration; private SshServer sshd; private String serverPubkey; @@ -43,10 +48,16 @@ public class Sshd { private boolean heartbeatOn; private long heartbeatPeriod; - public void start(BaguetteServerProperties configuration, ServerCoordinator coordinator) throws IOException { + 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(); @@ -64,12 +75,13 @@ public class Sshd { sshd.setShellFactory( new Factory() { private ServerCoordinator coordinator; + private NodeRegistry nodeRegistry; public Command create() { - ClientShellCommand msc = new ClientShellCommand(this.coordinator, configuration.isClientAddressOverrideAllowed()); - //msc.setId( "#-"+System.currentTimeMillis() ); - log.debug("SSH server: Shell Factory: create invoked : New ClientShellCommand id: {}", msc.getId()); - return msc; + ClientShellCommand csc = new ClientShellCommand(this.coordinator, configuration.isClientAddressOverrideAllowed(), eventBus, nodeRegistry); + //csc.setId( "#-"+System.currentTimeMillis() ); + log.debug("SSH server: Shell Factory: create invoked : New ClientShellCommand id: {}", csc.getId()); + return csc; } public Command get() { @@ -77,12 +89,13 @@ public class Sshd { return null; } - public Factory setCoordinator(ServerCoordinator coordinator) { + public Factory setCoordinatorAndNodeRegistry(ServerCoordinator coordinator, NodeRegistry nodeRegistry) { this.coordinator = coordinator; + this.nodeRegistry = nodeRegistry; return this; } } - .setCoordinator(coordinator) + .setCoordinatorAndNodeRegistry(coordinator, nodeRegistry) ); sshd.setPasswordAuthenticator( @@ -157,7 +170,7 @@ public class Sshd { String msg = String.format("Heartbeat %d", System.currentTimeMillis()); log.debug("--> Heartbeat: {}", msg); for (ClientShellCommand csc : ClientShellCommand.getActive()) { - csc.sendToClient(msg); + csc.sendToClient(msg, Level.DEBUG); } } log.info("--> Heartbeat: Stopped"); @@ -201,13 +214,29 @@ public class Sshd { } } - public Object readFromClient(String clientId, String 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.info("SSH server: Sending and Reading to/from client {} : {}", csc.getId(), command); - return csc.readFromClient(command); + log.debug("SSH server: Sending and Reading to/from client {} : {}", csc.getId(), command); + return csc.readFromClient(command, logLevel); } } return null; @@ -257,8 +286,8 @@ public class Sshd { String serverKeyFilePath = configuration.getServerKeyFile(); log.debug("_loadPubkeyAndFingerprint(): Server Key file: {}", serverKeyFilePath); File serverKeyFile = new File(serverKeyFilePath); - SimpleGeneratorHostKeyProvider z = new SimpleGeneratorHostKeyProvider(serverKeyFile); - z.loadKeys().forEach(kp -> { + SimpleGeneratorHostKeyProvider simpleGeneratorHostKeyProvider = new SimpleGeneratorHostKeyProvider(serverKeyFile); + simpleGeneratorHostKeyProvider.loadKeys().forEach(kp -> { log.debug("_loadPubkeyAndFingerprint(): KeyPair found: {}", kp.toString()); PublicKey serverKey = kp.getPublic(); log.debug("_loadPubkeyAndFingerprint(): Pubkey: {}", kp.toString()); @@ -274,7 +303,7 @@ public class Sshd { log.debug("_loadPubkeyAndFingerprint(): Fingerprint: {}", serverPubkeyFingerprint); } catch (Exception ex) { - log.error("_loadPubkeyAndFingerprint(): EXCEPTION: {}", ex); + log.error("_loadPubkeyAndFingerprint(): EXCEPTION: ", ex); } }); } diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/NoopCoordinator.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/NoopCoordinator.java index 0dff0791d31e4c950d4a6377c142c45a2454d3d9..5bdb0466c06329f68835eca7ed329facd1b18872 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/NoopCoordinator.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/NoopCoordinator.java @@ -11,6 +11,7 @@ package eu.melodic.event.baguette.server.coordinator; import eu.melodic.event.baguette.server.BaguetteServer; import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import eu.melodic.event.baguette.server.ServerCoordinator; import eu.melodic.event.baguette.server.properties.BaguetteServerProperties; import eu.melodic.event.translate.TranslationContext; @@ -62,6 +63,11 @@ public class NoopCoordinator implements ServerCoordinator { return -1; } + @Override + public synchronized void preregister(NodeRegistryEntry entry) { + _logInvocation("preregister", entry, true); + } + @Override public synchronized void register(ClientShellCommand c) { _logInvocation("register", c, true); @@ -77,16 +83,21 @@ public class NoopCoordinator implements ServerCoordinator { _logInvocation("clientReady", c, true); } - protected boolean _logInvocation(String methodName, ClientShellCommand c, boolean checkStarted) { + protected boolean _logInvocation(String methodName, Object o, boolean checkStarted) { String className = getClass().getSimpleName(); - String cscStr = (c!=null) ? String.format(". CSC: %s", c.toString()) : ""; + 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, cscStr); + log.warn("{}: {}(): Coordinator has not been started{}", className, methodName, str); } else if (!checkStarted && started) { - log.warn("{}: {}(): Coordinator is already running{}", className, methodName, cscStr); + log.warn("{}: {}(): Coordinator is already running{}", className, methodName, str); } else { - log.info("{}: {}(): Method invoked{}", className, methodName, cscStr); + log.info("{}: {}(): Method invoked{}", className, methodName, str); } return started; } diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/AtLeastTwoZoneManagementStrategy.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/AtLeastTwoZoneManagementStrategy.java index 91e820b379dafca7dfedd1ed4012d10d4d12d250..1406f0e82a5c14269e7924fe33c9bceb74706514 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/AtLeastTwoZoneManagementStrategy.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/AtLeastTwoZoneManagementStrategy.java @@ -10,10 +10,13 @@ package eu.melodic.event.baguette.server.coordinator.cluster; import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.baguette.server.NodeRegistryEntry; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.internal.guava.InetAddresses; +import java.util.Map; import java.util.UUID; /** @@ -37,30 +40,7 @@ public class AtLeastTwoZoneManagementStrategy implements IZoneManagementStrategy } @Override - public String getZoneIdFor(ClientShellCommand c) { - String nodeAddress = c.getClientIpAddress(); - String hostname = c.getClientHostname(); - log.debug("getZoneIdFor: {}: address: {}", c.getId(), nodeAddress); - log.debug("getZoneIdFor: {}: hostname: {}", c.getId(), hostname); - String zoneName = null; - if (StringUtils.isNotBlank(hostname) && !InetAddresses.isUriInetAddress(hostname)) { - int p = hostname.indexOf("."); - if (p>0) - zoneName = hostname.substring(p+1); - } - if (StringUtils.isBlank(zoneName) && StringUtils.isNotBlank(nodeAddress)) { - int p = nodeAddress.lastIndexOf("."); - if (p<0) p = nodeAddress.lastIndexOf(":"); - if (p>0) - zoneName = nodeAddress.substring(0, p); - } - return StringUtils.isBlank(zoneName) - ? UUID.randomUUID().toString() - : zoneName.replaceAll("[^A-Za-z0-9_]","_"); - } - - @Override - public synchronized void nodeAdded(ClientShellCommand csc, ClusteringCoordinator coordinator, ClusterZone zone) { + public synchronized void nodeAdded(ClientShellCommand csc, ClusteringCoordinator coordinator, IClusterZone zone) { if (zone.getNodes().size() < 2) return; @@ -83,18 +63,18 @@ public class AtLeastTwoZoneManagementStrategy implements IZoneManagementStrategy } } - private void joinToCluster(ClientShellCommand csc, ClusteringCoordinator coordinator, ClusterZone 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"); + //coordinator.sleep(1000); + //csc.sendCommand("CLUSTER-TEST"); } @Override - public synchronized void nodeRemoved(ClientShellCommand csc, ClusteringCoordinator coordinator, ClusterZone zone) { + 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); diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusterZone.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusterZone.java index 455450fbeb3e72238e4e6eec694688db129ffa93..2db0c3e273672fe9bdd0e26d46f392173722d07b 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusterZone.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusterZone.java @@ -10,6 +10,8 @@ package eu.melodic.event.baguette.server.coordinator.cluster; import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.baguette.server.NodeRegistryEntry; +import eu.melodic.event.util.ClientConfiguration; import eu.melodic.event.util.KeystoreUtil; import lombok.*; import lombok.extern.slf4j.Slf4j; @@ -23,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Data -public class ClusterZone { +public class ClusterZone implements IClusterZone { private final String id; private final int startPort; private final int endPort; @@ -34,6 +36,8 @@ public class ClusterZone { 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; @@ -44,7 +48,7 @@ public class ClusterZone { private ClientShellCommand aggregator; @SneakyThrows - public ClusterZone(@NotBlank String id, int startPort, int endPort) { + public ClusterZone(@NotBlank String id, int startPort, int endPort, String keystoreFileName) { checkArgs(id, startPort, endPort); this.id = id; this.startPort = startPort; @@ -52,8 +56,7 @@ public class ClusterZone { currentPort.set(startPort); this.clusterId = RandomStringUtils.randomAlphanumeric(64); - String fileName = String.format("logs/cluster_%d_%s.p12", System.currentTimeMillis(), id); - this.clusterKeystoreFile = new File(fileName); + this.clusterKeystoreFile = new File(keystoreFileName); this.clusterKeystoreType = "JKS"; this.clusterKeystorePassword = RandomStringUtils.randomAlphanumeric(64); log.info("New ClusterZone: zone: {}", id); @@ -66,7 +69,8 @@ public class ClusterZone { .createIfNotExist() .createKeyAndCert(clusterId, "CN=" + clusterId, "") .readFileAsBase64(); - log.debug(" Base64 content: {}", clusterKeystoreBase64); + log.debug(" Base64 content: {}", + StringUtils.isNotBlank(clusterKeystoreBase64) ? "Not empty" : "!!! Empty !!!"); } private void checkArgs(String id, int startPort, int endPort) { @@ -92,10 +96,12 @@ public class ClusterZone { 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); } } @@ -104,9 +110,15 @@ public class ClusterZone { nodes.remove(csc.getClientIpAddress()); if (csc.getClientZone()==this) csc.setClientZone(null); + if (csc.getNodeRegistryEntry()!=null && csc.getNodeRegistryEntry().getClusterZone()==this) + csc.getNodeRegistryEntry().setClusterZone(null); } } + public Set getNodeAddresses() { + return new HashSet<>(nodes.keySet()); + } + public List getNodes() { return new ArrayList<>(nodes.values()); } @@ -114,4 +126,54 @@ public class ClusterZone { public ClientShellCommand getNodeByAddress(String address) { return nodes.get(address); } + + // 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/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusterZoneDetector.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusterZoneDetector.java new file mode 100644 index 0000000000000000000000000000000000000000..bb0388745709746f39f273e2764cf15da4341ea4 --- /dev/null +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusterZoneDetector.java @@ -0,0 +1,166 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.server.coordinator.cluster; + +import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.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/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusteringCoordinator.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusteringCoordinator.java index c449859fba1dfe6ade407b2ceda28f8a1440d016..ef8bb32bb3b8d0225d483dbc76e275c38a562ee8 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusteringCoordinator.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/ClusteringCoordinator.java @@ -14,10 +14,13 @@ import eu.melodic.event.baguette.server.ClientShellCommand; import eu.melodic.event.baguette.server.NodeRegistryEntry; import eu.melodic.event.baguette.server.coordinator.NoopCoordinator; import eu.melodic.event.translate.TranslationContext; +import eu.melodic.event.util.ClientConfiguration; import eu.melodic.event.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; @@ -30,22 +33,41 @@ public class ClusteringCoordinator extends NoopCoordinator { 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<>(); + + 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)) @@ -74,6 +96,22 @@ public class ClusteringCoordinator extends NoopCoordinator { ? 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.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 @@ -85,7 +123,10 @@ public class ClusteringCoordinator extends NoopCoordinator { 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); @@ -94,7 +135,7 @@ public class ClusteringCoordinator extends NoopCoordinator { } private ClusterZone findZone(ClientShellCommand csc) { - String zoneId = zoneManagementStrategy.getZoneIdFor(csc); + String zoneId = clusterZoneDetector.getZoneIdFor(csc); return topologyMap.get(zoneId); } @@ -113,26 +154,84 @@ public class ClusteringCoordinator extends NoopCoordinator { 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()); + } + } + + 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; } + if (preregEntry!=null) csc.setNodeRegistryEntry(preregEntry); // 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); @@ -153,6 +252,12 @@ public class ClusteringCoordinator extends NoopCoordinator { } 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); @@ -166,6 +271,13 @@ public class ClusteringCoordinator extends NoopCoordinator { 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); @@ -181,15 +293,15 @@ public class ClusteringCoordinator extends NoopCoordinator { log.info("ClusteringCoordinator: --------------------------------------------------"); sleep(500); - // Add registered node in topology map - addNodeInTopology(csc); + // Registered node added in topology map - Notify ZoneManagementStrategy + addedNodeInTopology(csc); } private synchronized void addNodeInTopology(ClientShellCommand csc) { // Assign client in a zone - String zoneId = zoneManagementStrategy.getZoneIdFor(csc); + String zoneId = clusterZoneDetector.getZoneIdFor(csc); log.debug("addNodeInTopology: New client: id={}, address={}, zone-id={}", csc.getId(), csc.getClientIpAddress(), zoneId); - ClusterZone zone = topologyMap.computeIfAbsent(zoneId, id -> new ClusterZone(id, zoneStartPort, zoneEndPort)); + 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()); @@ -205,9 +317,11 @@ public class ClusteringCoordinator extends NoopCoordinator { //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, zone); + zoneManagementStrategy.nodeAdded(csc, this, csc.getClientZone()); log.info("addNodeInTopology: Client added in topology: client={}, address={}", csc.getId(), csc.getClientIpAddress()); } @@ -218,7 +332,7 @@ public class ClusteringCoordinator extends NoopCoordinator { private synchronized void removeNodeFromTopology(ClientShellCommand csc) { // Assign client in a zone - String zoneId = zoneManagementStrategy.getZoneIdFor(csc); + String zoneId = clusterZoneDetector.getZoneIdFor(csc); ClusterZone zone = topologyMap.get(zoneId); if (zone == null) { log.warn("removeNodeFromTopology: Not Registered client removed: client={}, address={}", csc.getId(), csc.getClientIpAddress()); @@ -235,7 +349,7 @@ public class ClusteringCoordinator extends NoopCoordinator { // Methods to be used by Zone Management Strategies // ------------------------------------------------------------------------ - void sendClusterKey(ClientShellCommand csc, ClusterZone zoneInfo) { + 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())); @@ -247,7 +361,7 @@ public class ClusteringCoordinator extends NoopCoordinator { zoneNodes.forEach(c -> c.sendCommand(command)); } - void instructClusterJoin(ClientShellCommand csc, ClusterZone zone, boolean startElection) { + void instructClusterJoin(ClientShellCommand csc, IClusterZone zone, boolean startElection) { List zoneNodes = zone.getNodes(); log.debug("instructClusterJoin: Zone members: {}", zoneNodes); @@ -282,7 +396,7 @@ public class ClusteringCoordinator extends NoopCoordinator { csc.sendCommand("CLUSTER-JOIN "+command); } - void instructClusterLeave(ClientShellCommand csc, ClusterZone zone) { + void instructClusterLeave(ClientShellCommand csc, IClusterZone zone) { // Send cluster leave command log.debug("instructClusterLeave: Client {} @ {} leaves cluster: CLUSTER-LEAVE", csc.getId(), csc.getClientIpAddress()); try { @@ -293,7 +407,7 @@ public class ClusteringCoordinator extends NoopCoordinator { } } - void electAggregator(ClusterZone zone) { + void electAggregator(IClusterZone zone) { sendCommandToZone("CLUSTER-EXEC broker elect", zone.getNodes()); } } diff --git a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/DefaultZoneManagementStrategy.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/DefaultZoneManagementStrategy.java index 2acf7e5c1585ffa3cdcdf471e2f1e0b99512eb50..06af812998b3fbe7094561fcd3d290bf1c1fe671 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/DefaultZoneManagementStrategy.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/DefaultZoneManagementStrategy.java @@ -10,6 +10,7 @@ package eu.melodic.event.baguette.server.coordinator.cluster; import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.internal.guava.InetAddresses; @@ -36,47 +37,24 @@ public class DefaultZoneManagementStrategy implements IZoneManagementStrategy { } @Override - public String getZoneIdFor(ClientShellCommand c) { - String nodeAddress = c.getClientIpAddress(); - String hostname = c.getClientHostname(); - log.debug("getZoneIdFor: {}: address: {}", c.getId(), nodeAddress); - log.debug("getZoneIdFor: {}: hostname: {}", c.getId(), hostname); - String zoneName = null; - if (StringUtils.isNotBlank(hostname) && !InetAddresses.isUriInetAddress(hostname)) { - int p = hostname.indexOf("."); - if (p>0) - zoneName = hostname.substring(p+1); - } - if (StringUtils.isBlank(zoneName) && StringUtils.isNotBlank(nodeAddress)) { - int p = nodeAddress.lastIndexOf("."); - if (p<0) p = nodeAddress.lastIndexOf(":"); - if (p>0) - zoneName = nodeAddress.substring(0, p); - } - return StringUtils.isBlank(zoneName) - ? UUID.randomUUID().toString() - : zoneName.replaceAll("[^A-Za-z0-9_]","_"); - } - - @Override - public synchronized void nodeAdded(ClientShellCommand csc, ClusteringCoordinator coordinator, ClusterZone zone) { + 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, ClusterZone 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"); + //coordinator.sleep(1000); + //csc.sendCommand("CLUSTER-TEST"); } @Override - public synchronized void nodeRemoved(ClientShellCommand csc, ClusteringCoordinator coordinator, ClusterZone zone) { + 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/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IClusterZone.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IClusterZone.java new file mode 100644 index 0000000000000000000000000000000000000000..d2edcb2c5c368b35e39438dd730e9c74e02839a5 --- /dev/null +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IClusterZone.java @@ -0,0 +1,42 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.server.coordinator.cluster; + +import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.baguette.server.NodeRegistryEntry; +import eu.melodic.event.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); + + 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/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IClusterZoneDetector.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IClusterZoneDetector.java new file mode 100644 index 0000000000000000000000000000000000000000..5a1eaa237cdf846dcfc9182af18bc4dfcddc29e9 --- /dev/null +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IClusterZoneDetector.java @@ -0,0 +1,21 @@ +/* + * 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/ + */ + +package eu.melodic.event.baguette.server.coordinator.cluster; + +import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.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/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IZoneManagementStrategy.java b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IZoneManagementStrategy.java index 5b3d814fe21a25ad5bbaa7a564098decc595e05f..413548a3102fb5d12082fab976d5aebb91f411ef 100644 --- a/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IZoneManagementStrategy.java +++ b/event-management/baguette-server/src/main/java/eu/melodic/event/baguette/server/coordinator/cluster/IZoneManagementStrategy.java @@ -10,16 +10,22 @@ package eu.melodic.event.baguette.server.coordinator.cluster; import eu.melodic.event.baguette.server.ClientShellCommand; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import java.util.Map; public interface IZoneManagementStrategy { - String getZoneIdFor(ClientShellCommand csc); 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 nodeAdded(ClientShellCommand csc, ClusteringCoordinator coordinator, ClusterZone zoneInfo) { } - default void nodeRemoved(ClientShellCommand csc, ClusteringCoordinator coordinator, ClusterZone zoneInfo) { } + 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/event-management/bin/detect.sh b/event-management/bin/detect.sh new file mode 100644 index 0000000000000000000000000000000000000000..6c0209268eb5e4e66c7456ef2e0d6bdfd700c350 --- /dev/null +++ b/event-management/bin/detect.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# +# 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/ +# + +#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/event-management/bin/initialize-MELODIC-keystores.sh b/event-management/bin/initialize-MELODIC-keystores.sh index dca266bbadd5c5007eefae23caad582b0d738011..7e92769ab7416d9677b656c61ce9e316509783fc 100755 --- a/event-management/bin/initialize-MELODIC-keystores.sh +++ b/event-management/bin/initialize-MELODIC-keystores.sh @@ -23,12 +23,24 @@ echo Resolving Public IP addresses... 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 -#PUBLIC_IP='1.2.3.4' +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} @@ -68,7 +80,17 @@ 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" -EXT_SAN_FMT="SAN=dns:%s,dns:localhost,ip:127.0.0.1,ip:${PUBLIC_IP}" +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 diff --git a/event-management/bin/run.bat b/event-management/bin/run.bat index d156520fad98eabfc4f85a72cf894f8fe479135f..1deef354d906ff27b5c0e5f0742ec055dff88bc7 100644 --- a/event-management/bin/run.bat +++ b/event-management/bin/run.bat @@ -51,7 +51,7 @@ if "%LOG_FILE%"=="" ( ) :: Waiting CDO to come up... -if exist %MELODIC_CONFIG_DIR%\wait-for-cdo.bat ( +IF NOT DEFINED EMS_SKIP_WAIT_CDO IF EXIST %MELODIC_CONFIG_DIR%\wait-for-cdo.bat ( echo "Waiting CDO server to start..." %MELODIC_CONFIG_DIR%\wait-for-cdo.bat ) @@ -66,10 +66,10 @@ IF NOT DEFINED RESTART_EXIT_CODE set RESTART_EXIT_CODE=99 :_restart_ems rem Use when Esper is packaged in control-service.jar -rem java %JAVA_OPTS% -Djasypt.encryptor.password=%JASYPT_PASSWORD% -Duser.timezone=Europe/Warsaw -Djava.security.egd=file:/dev/urandom -jar %JARS_DIR%\control-service.jar --logging.config=file:%LOG_CONFIG_FILE% +rem java %JAVA_OPTS% -Djasypt.encryptor.password=%JASYPT_PASSWORD% -Duser.timezone=Europe/Athens -Djava.security.egd=file:/dev/urandom -jar %JARS_DIR%\control-service.jar --logging.config=file:%LOG_CONFIG_FILE% rem Use when Esper is NOT packaged in control-service.jar -java %JAVA_OPTS% -Djasypt.encryptor.password=%JASYPT_PASSWORD% -Duser.timezone=Europe/Warsaw -Djava.security.egd=file:/dev/urandom -cp %JARS_DIR%\control-service.jar -Dloader.path=%JARS_DIR%\esper-7.1.0.jar org.springframework.boot.loader.PropertiesLauncher -nolog --logging.config=file:%LOG_CONFIG_FILE% %* +java %JAVA_OPTS% -Djasypt.encryptor.password=%JASYPT_PASSWORD% -Djava.security.egd=file:/dev/urandom -cp %JARS_DIR%\control-service.jar -Dloader.path=%JARS_DIR%\esper-7.1.0.jar org.springframework.boot.loader.PropertiesLauncher -nolog --logging.config=file:%LOG_CONFIG_FILE% %* if errorlevel %RESTART_EXIT_CODE% ( echo Restarting EMS server... diff --git a/event-management/bin/run.sh b/event-management/bin/run.sh index 7aef86f03a72baca86ba9eb3273fdb40507b3cc9..939001435b95ce675d8c32710c8e221703eb2f9b 100755 --- a/event-management/bin/run.sh +++ b/event-management/bin/run.sh @@ -30,7 +30,7 @@ if [[ -z $PUBLIC_DIR ]]; then PUBLIC_DIR=$BASEDIR/public_resources; export PUBLI # Initialize keystores and certificate # Uncomment next line to generate BrokerCEP keystore, truststore and certificate before EMS server launch -# Modifying 'initialize-keystores.bat' script you can customize the certificate generation +# Modifying 'initialize-keystores.sh' script you can customize the certificate generation #./bin/initialize-keystores.sh # Read JASYPT password (decrypts encrypted configuration settings) @@ -54,7 +54,7 @@ if [[ -z "$LOG_FILE" ]]; then fi # Waiting CDO to come up... -if [[ -f $MELODIC_CONFIG_DIR/wait-for-cdo.sh ]]; then +if [[ -z ${EMS_SKIP_WAIT_CDO+x} ]] && [[ -f $MELODIC_CONFIG_DIR/wait-for-cdo.sh ]]; then echo "Waiting CDO server to start..." $MELODIC_CONFIG_DIR/wait-for-cdo.sh fi @@ -70,10 +70,10 @@ if [[ -z $RESTART_EXIT_CODE ]]; then RESTART_EXIT_CODE=99; export RESTART_EXIT_C retCode=$RESTART_EXIT_CODE while :; do # Use when Esper is packaged in control-service.jar - # java $JAVA_OPTS -Djasypt.encryptor.password=$JASYPT_PASSWORD -Duser.timezone=Europe/Warsaw -Djava.security.egd=file:/dev/urandom -jar $JARS_DIR/control-service/target/control-service.jar --logging.config=file:$LOG_CONFIG_FILE + # java $JAVA_OPTS -Djasypt.encryptor.password=$JASYPT_PASSWORD -Duser.timezone=Europe/Athens -Djava.security.egd=file:/dev/urandom -jar $JARS_DIR/control-service/target/control-service.jar --logging.config=file:$LOG_CONFIG_FILE # Use when Esper is NOT packaged in control-service.jar - java $JAVA_OPTS -Djasypt.encryptor.password=$JASYPT_PASSWORD -Duser.timezone=Europe/Warsaw -Djava.security.egd=file:/dev/urandom -cp ${JARS_DIR}/control-service.jar -Dloader.path=${JARS_DIR}/esper-7.1.0.jar org.springframework.boot.loader.PropertiesLauncher --logging.config=file:$LOG_CONFIG_FILE $* + java $JAVA_OPTS -Djasypt.encryptor.password=$JASYPT_PASSWORD -Djava.security.egd=file:/dev/urandom -cp ${JARS_DIR}/control-service.jar -Dloader.path=${JARS_DIR}/esper-7.1.0.jar org.springframework.boot.loader.PropertiesLauncher --logging.config=file:$LOG_CONFIG_FILE $* retCode=$? if [[ $retCode -eq $RESTART_EXIT_CODE ]]; then echo "Restarting EMS server..."; else break; fi diff --git a/event-management/bin/sysmon.sh b/event-management/bin/sysmon.sh new file mode 100644 index 0000000000000000000000000000000000000000..9d15d4a640c0e657f828ea13b336098e1585ebf6 --- /dev/null +++ b/event-management/bin/sysmon.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# 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/ +# + +# 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/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepService.java b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepService.java index b77d45d600f73f062db4c100d7ad0177f224e4e6..4466b0116ad24ea59a65eec268a969c647158c69 100644 --- a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepService.java +++ b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepService.java @@ -203,7 +203,7 @@ public class BrokerCepService { 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, new EventMap(eventMap)); + _publishEvent(connectionString, destinationName, EventMap.toEventMap(eventMap)); } public synchronized void publishEvent(String connectionString, String username, String password, String destinationName, Map eventMap) throws JMSException { @@ -308,12 +308,15 @@ public class BrokerCepService { MessageProducer producer = session.createProducer(destination); producer.setDeliveryMode(javax.jms.DeliveryMode.NON_PERSISTENT); - // Create a messages + // Create a message //ObjectMessage message = session.createObjectMessage(event); String payload = gson.toJson(event); log.trace("BrokerCepService.publishEvent(): Message payload: topic={}, payload={}", destination, 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); @@ -323,6 +326,22 @@ public class BrokerCepService { 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, "://",":"); } @@ -401,7 +420,7 @@ public class BrokerCepService { } public Map getBrokerCepStatistics() { - Map bcepStats = new HashMap<>(); + 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()); @@ -412,9 +431,7 @@ public class BrokerCepService { bcepStats.put("count-total-events-other", BrokerCepConsumer.getOtherEventCounter()); bcepStats.put("count-total-events-failures", BrokerCepConsumer.getEventFailuresCounter()); - Map statsMap = new HashMap<>(); - statsMap.put("broker-cep", bcepStats); - return statsMap; + return bcepStats; } public void clearBrokerCepStatistics() { diff --git a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepStatementSubscriber.java b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepStatementSubscriber.java index 395f633decd43e6919a6586b5dabf7e8aa0a501e..651836e5c9cec0642f16e87827b785069725fb18 100644 --- a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepStatementSubscriber.java +++ b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/BrokerCepStatementSubscriber.java @@ -50,19 +50,20 @@ public class BrokerCepStatementSubscriber implements StatementSubscriber { log.info("- New event received: subscriber={}, topic={}, payload={}", name, topic, eventMap); String localBrokerUrl = brokerCep.getBrokerCepProperties().getBrokerUrlForConsumer(); String username = brokerCep.getBrokerUsername(); - String password = passwordUtil.getPasswordEncoder().encode(brokerCep.getBrokerPassword()); + String password = brokerCep.getBrokerPassword(); + String passwordEncoded = passwordUtil.encodePassword(password); try { // Publish new event to Local Broker topic log.trace("- Publishing event to local broker: subscriber={}, local-broker={}, username={}, password={}, topic={}, payload={}", - name, localBrokerUrl, username, password, topic, eventMap); + name, localBrokerUrl, username, passwordEncoded, topic, eventMap); brokerCep.publishEvent(localBrokerUrl, username, password, topic, eventMap); log.debug("- Event published to local broker: subscriber={}, local-broker={}, username={}, password={}, topic={}, payload={}", - name, localBrokerUrl, username, password, topic, eventMap); + name, localBrokerUrl, username, passwordEncoded, topic, eventMap); countLocalPublish(true); } catch (Exception ex) { log.error("- New event: ERROR while publishing to local broker: subscriber={}, local-broker={}, username={}, password={}, topic={}, exception=", - name, localBrokerUrl, username, password, topic, ex); + name, localBrokerUrl, username, passwordEncoded, topic, ex); countLocalPublish(false); } } diff --git a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerAdvisoryWatcher.java b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerAdvisoryWatcher.java index b7a5d3a0d69ad0bdc1dac928a7cd68f3e5f2acc0..faf6cd3586bba57ab5ee3e0cc4fba17b05816c25 100644 --- a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerAdvisoryWatcher.java +++ b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerAdvisoryWatcher.java @@ -20,9 +20,11 @@ import org.apache.activemq.command.DestinationInfo; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import javax.jms.*; +import java.time.Instant; @ConditionalOnProperty(name="brokercep.enable-advisory-watcher", matchIfMissing = true) @Service @@ -36,12 +38,17 @@ public class BrokerAdvisoryWatcher implements MessageListener, InitializingBean private BrokerCepService brokerCerService; @Autowired private PasswordUtil passwordUtil; + @Autowired + private TaskScheduler taskScheduler; + + private final int initRetryDelay = 5; // in seconds private Connection connection; private Session session; @Override public void afterPropertiesSet() { + log.debug("BrokerAdvisoryWatcher: afterPropertiesSet: BrokerCepProperties: {}", brokerCerService.getBrokerCepProperties()); initialize(); } @@ -72,7 +79,9 @@ public class BrokerAdvisoryWatcher implements MessageListener, InitializingBean consumer.setMessageListener( this ); log.debug("BrokerAdvisoryWatcher.init(): Initializing instance... done"); } catch (Exception ex) { - log.error("BrokerAdvisoryWatcher.init(): EXCEPTION: ", ex); + log.error("BrokerAdvisoryWatcher.init(): EXCEPTION: while retry in {} seconds:", initRetryDelay, ex); + final BrokerAdvisoryWatcher _this = this; + taskScheduler.schedule(_this::initialize, Instant.now().plusSeconds(initRetryDelay)); } } diff --git a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerConfig.java b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerConfig.java index 9f12991e57e6baecbbe7c5d8bc400a99c15a490a..4b10425f12a68eaabaa2678886818284542288ed 100644 --- a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerConfig.java +++ b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/BrokerConfig.java @@ -13,6 +13,7 @@ import eu.melodic.event.brokercep.broker.interceptor.AbstractMessageInterceptor; import eu.melodic.event.brokercep.properties.BrokerCepProperties; import eu.melodic.event.util.KeystoreUtil; import eu.melodic.event.util.PasswordUtil; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ActiveMQSslConnectionFactory; @@ -29,7 +30,6 @@ 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.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -51,10 +51,11 @@ import java.util.stream.Collectors; //import org.apache.activemq.security.JaasAuthenticationPlugin; +@Slf4j @Service -@Configuration @EnableJms -@Slf4j +@Configuration +@RequiredArgsConstructor public class BrokerConfig implements InitializingBean { private final static int LOCAL_ADMIN_INDEX = 0; @@ -64,12 +65,9 @@ public class BrokerConfig implements InitializingBean { private final static int USERNAME_RANDOM_PART_LENGTH = 10; private final static int PASSWORD_LENGTH = 20; - @Autowired - private BrokerCepProperties properties; - @Autowired - private PasswordUtil passwordUtil; - @Autowired - private ApplicationContext applicationContext; + private final BrokerCepProperties properties; + private final PasswordUtil passwordUtil; + private final ApplicationContext applicationContext; private SimpleAuthenticationPlugin brokerAuthenticationPlugin; private SimpleBrokerAuthorizationPlugin brokerAuthorizationPlugin; @@ -133,10 +131,8 @@ public class BrokerConfig implements InitializingBean { brokerAuthenticationPlugin = sap; if (log.isDebugEnabled()) { - log.debug("BrokerConfig._initializeSecurity(): Initialized broker authentication plugin: anonymous-access={}, user-credentials={}", - sap.isAnonymousAccessAllowed(), - sap.getUserPasswords().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> passwordUtil.encodePassword(e.getValue()))) + log.debug("BrokerConfig._initializeSecurity(): Initialized broker authentication plugin: anonymous-access={}, user-list={}", + sap.isAnonymousAccessAllowed(), sap.getUserPasswords().keySet() ); } } @@ -170,9 +166,11 @@ public class BrokerConfig implements InitializingBean { 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().getKeyEntryNameValue()); log.trace("BrokerConfig.initializeKeyAndCert(): Retrieving certificate for Broker-SSL: file={}, type={}, password={}, alias={}, cert=\n{}", - properties.getSsl().getKeystoreFile(), properties.getSsl().getKeystoreType(), properties.getSsl().getKeystorePassword(), + properties.getSsl().getKeystoreFile(), properties.getSsl().getKeystoreType(), + passwordUtil.encodePassword(properties.getSsl().getKeystorePassword()), properties.getSsl().getKeyEntryNameValue(), this.brokerCert); log.info("BrokerConfig.initializeKeyAndCert(): Initializing keystore, truststore and certificate for Broker-SSL... done"); } @@ -349,6 +347,11 @@ public class BrokerConfig implements InitializingBean { 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.info("BrokerConfig: Message interceptors initializing..."); List interceptorSpecs = properties.getMessageInterceptors() .stream() diff --git a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/interceptor/SourceAddressMessageUpdateInterceptor.java b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/interceptor/SourceAddressMessageUpdateInterceptor.java index 7e8a3625d6bb44b0bce080050d17b7961057f38e..2642a006d92b2a532ff566e70fb53ba5a720802d 100644 --- a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/interceptor/SourceAddressMessageUpdateInterceptor.java +++ b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/broker/interceptor/SourceAddressMessageUpdateInterceptor.java @@ -9,6 +9,7 @@ package eu.melodic.event.brokercep.broker.interceptor; +import eu.melodic.event.util.EmsConstant; import eu.melodic.event.util.NetUtil; import lombok.extern.slf4j.Slf4j; import org.apache.activemq.broker.Connection; @@ -17,12 +18,18 @@ import org.apache.commons.lang3.StringUtils; @Slf4j public class SourceAddressMessageUpdateInterceptor extends AbstractMessageInterceptor { - private final String sourceAddressPropertyName = "producer-host"; + private final String sourceAddressPropertyName = EmsConstant.EVENT_PROPERTY_SOURCE_ADDRESS; @Override public void intercept(Message message) { log.trace("SourceAddressMessageUpdateInterceptor: Message: {}", message); try { + Object sourceProperty = message.getProperty(sourceAddressPropertyName); + 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); diff --git a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/event/EventMap.java b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/event/EventMap.java index 0394e337c3cffa2dc5945a9493a5c792a115b959..6178295f3a2e06857e6fa4a851d80cabb719dfac 100644 --- a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/event/EventMap.java +++ b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/event/EventMap.java @@ -12,6 +12,7 @@ package eu.melodic.event.brokercep.event; import com.google.gson.Gson; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -22,6 +23,7 @@ import java.util.stream.Collectors; @Data @Slf4j +@NoArgsConstructor @EqualsAndHashCode(callSuper = false) public class EventMap extends LinkedHashMap implements Serializable { @@ -65,16 +67,34 @@ public class EventMap extends LinkedHashMap implements Serializa } + // 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() { + /*public EventMap() { super(); - } + put(TIMESTAMP_NAME, System.currentTimeMillis()); + }*/ public EventMap(Map 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)); + } } public EventMap(double metricValue) { @@ -94,6 +114,13 @@ public class EventMap extends LinkedHashMap implements Serializa } + // 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((Map) o); + return parseEventMap(o.toString()); + } + // Parse from string public static EventMap parseEventMap(@NonNull String s) { /*if (s==null) return null; diff --git a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/properties/BrokerCepProperties.java b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/properties/BrokerCepProperties.java index d8553c774a2efc7d7d288938c1a4abe3bcb2bada..667fe90c13ce404418927da18029d5429c8e1bcd 100644 --- a/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/properties/BrokerCepProperties.java +++ b/event-management/broker-cep/src/main/java/eu/melodic/event/brokercep/properties/BrokerCepProperties.java @@ -61,6 +61,7 @@ public class BrokerCepProperties { @Value("${authentication-enabled:false}") private boolean authenticationEnabled; + @ToString.Exclude @Value("${additional-broker-credentials:}") private String additionalBrokerCredentials; @Value("${authorization-enabled:false}") @@ -108,6 +109,7 @@ public class BrokerCepProperties { public static class ForwardDestinationConfig { private String connectionString; private String username; + @ToString.Exclude private String password; } } diff --git a/event-management/broker-client/pom.xml b/event-management/broker-client/pom.xml index 02a140f8547b9a9b080fafc97b8c67098a042405..69dfb8229b470bb30a550dcc78cc256f556c404b 100644 --- a/event-management/broker-client/pom.xml +++ b/event-management/broker-client/pom.xml @@ -21,6 +21,13 @@ https://gitlab.ow2.org/melodic/melodic-upperware/-/tree/master/event-management/broker-client + + + eu.melodic.event + util + ${project.version} + + org.springframework.boot diff --git a/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClient.java b/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClient.java index 1aa5bf5756c1cd0ccea2b08c5dcb9f4e5fe45e35..28a1f408d1778bdf4dbdee9d76a7670882c1ac10 100644 --- a/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClient.java +++ b/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClient.java @@ -13,6 +13,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import eu.melodic.event.brokerclient.event.EventMap; import eu.melodic.event.brokerclient.properties.BrokerClientProperties; +import eu.melodic.event.util.PasswordUtil; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.activemq.ActiveMQConnection; @@ -41,6 +42,8 @@ public class BrokerClient { @Autowired private BrokerClientProperties properties; + @Autowired + private PasswordUtil passwordUtil; private Connection connection; private Session session; private HashMap listeners = new HashMap<>(); @@ -57,6 +60,20 @@ public class BrokerClient { 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 { @@ -87,7 +104,7 @@ public class BrokerClient { } // initialize broker client - BrokerClient client = new BrokerClient(p); + BrokerClient client = new BrokerClient(p, new PasswordUtil()); log.info("BrokerClient: Default Configuration:\n{}", client.properties); return client; @@ -179,13 +196,43 @@ public class BrokerClient { _publishEvent(connectionString, destinationName, messageType, eventContents, propertiesMap); } - @SneakyThrows + 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) { - openConnection(connectionString); + if (StringUtils.isBlank(username)) + openConnection(connectionString); + else + openConnection(connectionString, username, password); _closeConn = ! properties.isPreserveConnection(); } @@ -387,11 +434,11 @@ public class BrokerClient { public synchronized void openConnection(String connectionString, String username, String password, boolean preserveConnection) throws JMSException { checkProperties(); if (connectionString == null) connectionString = properties.getBrokerUrl(); - log.debug("BrokerClient: Credetials provided as arguments: username={}, password={}", username, password); + 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: Credetials read from properties: username={}, password={}", username, password); + log.debug("BrokerClient: Credentials read from properties: username={}, password={}", username, passwordUtil.encodePassword(password)); } // Create connection factory @@ -401,15 +448,15 @@ public class BrokerClient { connectionFactory.setUserName(username); connectionFactory.setPassword(password); } - log.debug("BrokerClient: Connection credentials: username={}, password={}", username, password); + log.debug("BrokerClient: Connection credentials: username={}, password={}", username, passwordUtil.encodePassword(password)); // Create a Connection - log.info("BrokerClient: Connecting to broker: {}...", connectionString); + log.debug("BrokerClient: Connecting to broker: {}...", connectionString); Connection connection = connectionFactory.createConnection(); connection.start(); // Create a Session - log.info("BrokerClient: Opening session..."); + log.debug("BrokerClient: Opening session..."); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); this.connection = connection; diff --git a/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClientApp.java b/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClientApp.java index b33d1d0b14fc09db12b659e1c195aa29b7ac8dbc..6b7ff87f141b5247d00f0db6297aab7b7a5761f6 100644 --- a/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClientApp.java +++ b/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/BrokerClientApp.java @@ -168,12 +168,12 @@ public class BrokerClientApp { BrokerClient client = BrokerClient.newClient(); client.openConnection(url, username, password, true); - EventGenerator generator = new EventGenerator(); - generator.setClient(client); + EventGenerator generator = new EventGenerator(client); + //generator.setClient(client); generator.setBrokerUrl(url); generator.setDestinationName(topic); generator.setInterval(interval); - generator.setHowmany(howmany); + generator.setHowMany(howmany); generator.setLowerValue(lowerValue); generator.setUpperValue(upperValue); generator.setLevel(level); diff --git a/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/event/EventGenerator.java b/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/event/EventGenerator.java index 5dcdd0bc92c416f92f80bca4c270b73a10d65418..3151a450128a47ce75e71124d4d4363903de0935 100644 --- a/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/event/EventGenerator.java +++ b/event-management/broker-client/src/main/java/eu/melodic/event/brokerclient/event/EventGenerator.java @@ -12,21 +12,37 @@ package eu.melodic.event.brokerclient.event; import eu.melodic.event.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; -@Data @Slf4j +@Data +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class EventGenerator implements Runnable { - private BrokerClient client; + 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 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); @@ -49,12 +65,12 @@ public class EventGenerator implements Runnable { double newValue = Math.random() * valueRangeWidth + lowerValue; EventMap event = new EventMap(newValue, level, System.currentTimeMillis()); log.info("EventGenerator.run(): Sending event #{}: {}", countSent + 1, event); - client.publishEvent(brokerUrl, destinationName, event); + client.publishEventWithCredentials(brokerUrl, brokerUsername, brokerPassword, destinationName, event); countSent++; - if (countSent == howmany) keepRunning = false; + if (countSent == howMany) keepRunning = false; log.info("EventGenerator.run(): Event sent #{}: {}", countSent, event); } catch (Exception ex) { - log.warn("EventGenerator.run(): WHILE-EXCEPTION: {}", ex); + log.warn("EventGenerator.run(): WHILE-EXCEPTION: ", ex); } // sleep for 'interval' ms try { diff --git a/event-management/common/pom.xml b/event-management/common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..48034aecb4ed9ebb6dff2ab9e4f1952931144555 --- /dev/null +++ b/event-management/common/pom.xml @@ -0,0 +1,44 @@ + + + + event-management + eu.melodic.event + 4.5.0-SNAPSHOT + + 4.0.0 + + common + Upperware - EMS - Common to EMS server and clients + + + + + eu.melodic.event + broker-cep + ${project.version} + + + + + org.springframework + spring-web + + + + + org.projectlombok + lombok + provided + + + + \ No newline at end of file diff --git a/event-management/common/src/main/java/eu/melodic/event/common/collector/CollectorContext.java b/event-management/common/src/main/java/eu/melodic/event/common/collector/CollectorContext.java new file mode 100644 index 0000000000000000000000000000000000000000..a2498ddc751f6bad694ab7b96a082c2dcebbdda8 --- /dev/null +++ b/event-management/common/src/main/java/eu/melodic/event/common/collector/CollectorContext.java @@ -0,0 +1,24 @@ +/* + * 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/ + */ + +package eu.melodic.event.common.collector; + +import eu.melodic.event.brokercep.event.EventMap; +import eu.melodic.event.util.ClientConfiguration; + +import java.io.Serializable; +import java.util.List; +import java.util.Set; + +public interface CollectorContext { + List getNodeConfigurations(); + Set getNodesWithoutClient(); + boolean isAggregator(); + boolean sendEvent(String connectionString, String destinationName, EventMap event, boolean createDestination); +} diff --git a/event-management/common/src/main/java/eu/melodic/event/common/collector/netdata/NetdataCollector.java b/event-management/common/src/main/java/eu/melodic/event/common/collector/netdata/NetdataCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..e14238c95eccae1957a91146a54664eda2326f30 --- /dev/null +++ b/event-management/common/src/main/java/eu/melodic/event/common/collector/netdata/NetdataCollector.java @@ -0,0 +1,302 @@ +/* + * 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/ + */ + +package eu.melodic.event.common.collector.netdata; + +import eu.melodic.event.brokercep.event.EventMap; +import eu.melodic.event.common.collector.CollectorContext; +import eu.melodic.event.util.EmsConstant; +import eu.melodic.event.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.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.client.RestTemplate; + +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.stream.Collectors; + +/** + * Collects measurements from Netdata http server + */ +@Slf4j +@RequiredArgsConstructor +public class NetdataCollector implements InitializingBean, Runnable { + public final static String NETDATA_COLLECTION_START = "NETDATA_COLLECTION_START"; + public final static String NETDATA_COLLECTION_END = "NETDATA_COLLECTION_END"; + 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_PAUSED = "NETDATA_NODE_PAUSED"; + public final static String NETDATA_NODE_RESUMED = "NETDATA_NODE_RESUMED"; + + protected final NetdataCollectorProperties properties; + protected final CollectorContext collectorContext; + protected final TaskScheduler taskScheduler; + protected final EventBus eventBus; + + protected RestTemplate restTemplate = new RestTemplate(); + 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::Netdata: properties: {}", 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]: "")); + + this.restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(5)) + .build(); + } + + public synchronized void start() { + // check if already running + if (started) { + log.warn("Collectors::Netdata: Already started"); + return; + } + + // check parameters + if (properties==null || !properties.isEnable()) { + log.warn("Collectors::Netdata: Collector not enabled"); + return; + } + if (properties.getDelay()<0) properties.setDelay(0); + 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); + } + + log.info("Collectors::Netdata: configuration: {}", properties); + + // Schedule collection execution + errorsMap.clear(); + ignoredNodes.clear(); + runner = taskScheduler.scheduleWithFixedDelay(this, properties.getDelay()); + started = true; + + log.info("Collectors::Netdata: Started"); + } + + public synchronized void stop() { + if (!started) { + log.warn("Collectors::Netdata: Not started"); + return; + } + + // Cancel collection execution + started = false; + runner.cancel(true); + runner = null; + ignoredNodes.values().forEach(task -> task.cancel(true)); + log.info("Collectors::Netdata: Stopped"); + } + + public void run() { + if (!started) return; + + log.trace("Collectors::Netdata: run(): BEGIN"); + if (log.isTraceEnabled()) { + log.trace("Collectors::Netdata: run(): errors-map={}", errorsMap); + log.trace("Collectors::Netdata: run(): ignored-nodes={}", ignoredNodes.keySet()); + } + + // collect data from local node + if (! properties.isSkipLocal()) { + log.info("Collectors::Netdata: Collecting metrics from local node..."); + collectAndPublishData(""); + } else { + log.debug("Collectors::Netdata: Collection from local node is disabled"); + } + + // if Aggregator, collect data from nodes without client + log.trace("Collectors::Netdata: Nodes without clients in Zone: {}", collectorContext.getNodesWithoutClient()); + log.trace("Collectors::Netdata: Is Aggregator: {}", collectorContext.isAggregator()); + if (collectorContext.isAggregator()) { + if (collectorContext.getNodesWithoutClient().size()>0) { + log.info("Collectors::Netdata: Collecting metrics from remote nodes (without EMS client): {}", + collectorContext.getNodesWithoutClient()); + for (Serializable nodeAddress : collectorContext.getNodesWithoutClient()) { + // collect data from remote node + collectAndPublishData(nodeAddress.toString()); + } + } else + log.debug("Collectors::Netdata: No remote nodes (without EMS client)"); + } + + log.trace("Collectors::Netdata: run(): END"); + } + + private COLLECTION_RESULT collectAndPublishData(@NonNull String nodeAddress) { + if (ignoredNodes.containsKey(nodeAddress)) { + log.info("Collectors::Netdata: Node is in ignore list: {}", nodeAddress); + return COLLECTION_RESULT.IGNORED; + } + try { + sendEvent(NETDATA_COLLECTION_START, nodeAddress); + _collectAndPublishData(nodeAddress); + sendEvent(NETDATA_COLLECTION_END, nodeAddress); + + //if (Optional.ofNullable(errorsMap.put(nodeAddress, 0)).orElse(0)>0) sendEvent(NETDATA_CONN_OK, nodeAddress); + sendEvent(NETDATA_CONN_OK, nodeAddress); + return COLLECTION_RESULT.OK; + } catch (Throwable t) { + int errors = errorsMap.compute(nodeAddress, (k, v) -> Optional.ofNullable(v).orElse(0) + 1); + int errorLimit = properties.getErrorLimit(); + int pausePeriod = properties.getPausePeriod(); + log.warn("Collectors::Netdata: Exception while collecting metrics from node: {}, #errors={}, exception: {}", + nodeAddress, errors, getExceptionMessages(t)); + log.debug("Collectors::Netdata: Exception while collecting metrics from node: {}, #errors={}\n", nodeAddress, errors, t); + + sendEvent(NETDATA_CONN_ERROR, nodeAddress, "errors="+errors); + + if (errorLimit>0 && pausePeriod>0) { + if (errors >= errorLimit) { + log.warn("Collectors::Netdata: Too many consecutive errors occurred while attempting to collect metrics from node: {}, num-of-errors={}", nodeAddress, errors); + log.warn("Collectors::Netdata: Will pause metrics collection from node for {} seconds: {}", pausePeriod, nodeAddress); + ignoredNodes.put(nodeAddress, taskScheduler.schedule(() -> { + errorsMap.put(nodeAddress, 0); + ignoredNodes.remove(nodeAddress); + log.info("Collectors::Netdata: Resumed metrics collection from node: {}", nodeAddress); + sendEvent(NETDATA_NODE_RESUMED, nodeAddress); + }, Instant.now().plusSeconds(pausePeriod))); + + sendEvent(NETDATA_NODE_PAUSED, nodeAddress); + } + } else + log.debug("Collectors::Netdata: Metrics collection pausing is disabled"); + 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()); + } + + 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.info("Collectors::Netdata: Collecting data from url: {}", url); + + log.debug("Collectors::Netdata: Collecting data: {}...", url); + long startTm = System.currentTimeMillis(); + ResponseEntity response = restTemplate.getForEntity(url, HashMap.class); + long callEndTm = System.currentTimeMillis(); + log.trace("Collectors::Netdata: ...response: {}", response); + if (response.getStatusCode()==HttpStatus.OK) { + Map dataMap = response.getBody(); + boolean createTopic = properties.isCreateTopic(); + int countSuccess = 0; + int countErrors = 0; + log.trace("Collectors::Netdata: ...keys: {}", dataMap.keySet()); + 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); + try { + boolean createDestination = (createTopic || allowedTopics!=null && allowedTopics.contains(metricName)); + if (topicMap!=null) { + String targetTopic = topicMap.get(metricName); + if (targetTopic!=null && !targetTopic.isEmpty()) + metricName = targetTopic; + } + EventMap event = new EventMap(metricValue, 1, timestamp); + event.setEventProperty(EmsConstant.EVENT_PROPERTY_SOURCE_ADDRESS, nodeAddress); + log.debug("Collectors::Netdata: {}: {}", metricName, metricValue); + if (collectorContext.sendEvent(null, metricName, event, createDestination)) + countSuccess++; + } catch (Exception e) { + log.warn("Collectors::Netdata: Publishing netdata metric failed: ", e); + countErrors++; + } + } + } + + if (Thread.currentThread().isInterrupted()) break; + } + long endTm = System.currentTimeMillis(); + log.debug("Collectors::Netdata: Collecting data...ok"); + log.info("Collectors::Netdata: Metrics: extracted={}, published={}, failed={}", + countSuccess+countErrors, countSuccess, countErrors); + log.debug("Collectors::Netdata: Durations: rest-call={}, extract+publish={}, total={}", + callEndTm-startTm, endTm-callEndTm, endTm-startTm); + } else { + log.warn("Collectors::Netdata: Collecting data...failed: Http Status: {}", response.getStatusCode()); + } + } +} diff --git a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/netdata/NetdataCollectorProperties.java b/event-management/common/src/main/java/eu/melodic/event/common/collector/netdata/NetdataCollectorProperties.java similarity index 72% rename from event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/netdata/NetdataCollectorProperties.java rename to event-management/common/src/main/java/eu/melodic/event/common/collector/netdata/NetdataCollectorProperties.java index 294d1e232beaa9c4ec71219caaa2433c3923ce01..b73c6cf5356bb74f9ae409da63df4aad014bce05 100644 --- a/event-management/baguette-client/src/main/java/eu/melodic/event/baguette/client/collector/netdata/NetdataCollectorProperties.java +++ b/event-management/common/src/main/java/eu/melodic/event/common/collector/netdata/NetdataCollectorProperties.java @@ -7,13 +7,12 @@ * https://www.mozilla.org/en-US/MPL/2.0/ */ -package eu.melodic.event.baguette.client.collector.netdata; +package eu.melodic.event.common.collector.netdata; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; import java.util.List; @@ -21,11 +20,15 @@ import java.util.List; @Data @Configuration @ConfigurationProperties(prefix = "collector.netdata") -@PropertySource("file:${MELODIC_CONFIG_DIR}/baguette-client.properties") public class NetdataCollectorProperties { 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 disables collection pausing + private int pausePeriod; // in seconds. Zero or negative value disables collection pausing } diff --git a/event-management/config-files/baguette-client-install/linux/baguette-skip.json b/event-management/config-files/baguette-client-install/linux/baguette-skip.json new file mode 100644 index 0000000000000000000000000000000000000000..2941719e1ed9d89cc149042e221c56187d0fbb0c --- /dev/null +++ b/event-management/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/event-management/config-files/baguette-client-install/linux.json b/event-management/config-files/baguette-client-install/linux/baguette.json similarity index 83% rename from event-management/config-files/baguette-client-install/linux.json rename to event-management/config-files/baguette-client-install/linux/baguette.json index 9c309f36eac06b4e5b53e35cdceb28b3880ec244..32d882395c7074f4d62416dd71ec59328cdf41e2 100644 --- a/event-management/config-files/baguette-client-install/linux.json +++ b/event-management/config-files/baguette-client-install/linux/baguette.json @@ -1,7 +1,12 @@ { "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", @@ -37,7 +42,7 @@ "description": "Upload EMS client installation package", "taskType": "COPY", "fileName": "/tmp/baguette-client.tgz", - "localFileName": "${EMS_PUBLIC_DIR}/baguette-client.tgz", + "localFileName": "${EMS_PUBLIC_DIR}/resources/baguette-client.tgz", "executable": false, "exitCode": 0, "match": false @@ -46,7 +51,7 @@ "description": "Upload installation package MD5 checksum", "taskType": "COPY", "fileName": "/tmp/baguette-client.tgz.md5", - "localFileName": "${EMS_PUBLIC_DIR}/baguette-client.tgz.md5", + "localFileName": "${EMS_PUBLIC_DIR}/resources/baguette-client.tgz.md5", "executable": false, "exitCode": 0, "match": false @@ -128,11 +133,18 @@ { "description": "-- LIST baguette-client FILES --", "taskType": "CMD", - "command": "ls -lR /opt/baguette-client ", + "command": "ls -l /opt/baguette-client ", "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", diff --git a/event-management/config-files/baguette-client-install/linux/check-ignore.json b/event-management/config-files/baguette-client-install/linux/check-ignore.json new file mode 100644 index 0000000000000000000000000000000000000000..419f416db3717861fb128a42b7dbc40cc4f3cec8 --- /dev/null +++ b/event-management/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/event-management/config-files/baguette-client-install/linux/detect.json b/event-management/config-files/baguette-client-install/linux/detect.json new file mode 100644 index 0000000000000000000000000000000000000000..fb28239aabf6094a68e21ac840fd4a6540f9af86 --- /dev/null +++ b/event-management/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": { "pattern": "(^\\s*CPU_SOCKETS\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "CPU_CORES": { "pattern": "(^\\s*CPU_CORES\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "CPU_PROCESSORS": { "pattern": "(^\\s*CPU_PROCESSORS\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "RAM_TOTAL_KB": { "pattern": "(^\\s*RAM_TOTAL_KB\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "RAM_AVAILABLE_KB": { "pattern": "(^\\s*RAM_AVAILABLE_KB\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "RAM_FREE_KB": { "pattern": "(^\\s*RAM_FREE_KB\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "RAM_USED_KB": { "pattern": "(^\\s*RAM_USED_KB\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "RAM_UTILIZATION": { "pattern": "(^\\s*RAM_UTILIZATION\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "DISK_TOTAL_KB": { "pattern": "(^\\s*DISK_TOTAL_KB\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "DISK_FREE_KB": { "pattern": "(^\\s*DISK_FREE_KB\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "DISK_USED_KB": { "pattern": "(^\\s*DISK_USED_KB\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "DISK_UTILIZATION": { "pattern": "(^\\s*DISK_UTILIZATION\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "OS_ARCHITECTURE": { "pattern": "(^\\s*OS_ARCHITECTURE\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "OS_KERNEL": { "pattern": "(^\\s*OS_KERNEL\\s*[=:]\\s*(.*)\\s*)", "flags": 0 }, + "OS_KERNEL_RELEASE": { "pattern": "(^\\s*OS_KERNEL_RELEASE\\s*[=:]\\s*(.*)\\s*)", "flags": 0 } + } + }, + { + "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/event-management/config-files/baguette-client-install/jre8.json b/event-management/config-files/baguette-client-install/linux/jre8.json similarity index 87% rename from event-management/config-files/baguette-client-install/jre8.json rename to event-management/config-files/baguette-client-install/linux/jre8.json index 4b963016cde4d8d3b741a5fae63b1c9ba3168aa3..3a909408864f57e7c5a72b0182959673637da21e 100644 --- a/event-management/config-files/baguette-client-install/jre8.json +++ b/event-management/config-files/baguette-client-install/linux/jre8.json @@ -1,6 +1,7 @@ { "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", diff --git a/event-management/config-files/baguette-client-install/linux/netdata.json b/event-management/config-files/baguette-client-install/linux/netdata.json new file mode 100644 index 0000000000000000000000000000000000000000..715fd1419cd36cc2031bc995079cb55dfe2e3774 --- /dev/null +++ b/event-management/config-files/baguette-client-install/linux/netdata.json @@ -0,0 +1,34 @@ +{ + "os": "", + "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": "Download Netdata kickstart.sh", + "taskType": "CMD", + "command": "curl https://my-netdata.io/kickstart-static64.sh > /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 ", + "executionTimeout": 600000 + } + ] +} \ No newline at end of file diff --git a/event-management/config-files/baguette-client-install/linux/recover-baguette.json b/event-management/config-files/baguette-client-install/linux/recover-baguette.json new file mode 100644 index 0000000000000000000000000000000000000000..4d6c16df40f1800c4d12ab45ae38463e4e4ec4c7 --- /dev/null +++ b/event-management/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": "/opt/baguette-client/bin/kill.sh", + "executable": false, + "exitCode": 0, + "match": false, + "retries": 5 + }, + { + "description": "Starting new EMS client process", + "taskType": "CMD", + "command": "/opt/baguette-client/bin/run.sh", + "executable": false, + "exitCode": 0, + "match": false, + "retries": 5 + } + ] +} \ No newline at end of file diff --git a/event-management/config-files/baguette-client-install/linux-start.json b/event-management/config-files/baguette-client-install/linux/start-agents.json similarity index 72% rename from event-management/config-files/baguette-client-install/linux-start.json rename to event-management/config-files/baguette-client-install/linux/start-agents.json index 472ba3beabafcdb16f18faa92931d65768790d80..7f152894575e969099177c6700615dcd3ccaa6e8 100644 --- a/event-management/config-files/baguette-client-install/linux-start.json +++ b/event-management/config-files/baguette-client-install/linux/start-agents.json @@ -1,6 +1,7 @@ { "os": "LINUX", "description": "Starting Netdata and Baguette agents at VM node", + "condition": "! ${SKIP_START:-false}", "instructions": [ { "description": "Launch EMS client", @@ -14,7 +15,8 @@ { "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 -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, diff --git a/event-management/config-files/baguette-client-install/netdata.json b/event-management/config-files/baguette-client-install/netdata.json deleted file mode 100644 index ef3de8269f544a40b4f5305d55a7cacf36225e57..0000000000000000000000000000000000000000 --- a/event-management/config-files/baguette-client-install/netdata.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "os": "", - "description": "Netdata installation instruction set at VM node", - "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", - "executable": false, - "exitCode": 99, - "match": true, - "message": "Netdata is already installed at Node" - }, - { - "description": "Download and run Netdata kickstart.sh", - "taskType": "CMD", - "command": "bash <(curl -Ss https://my-netdata.io/kickstart.sh) --dont-wait --no-updates --disable-telemetry", - "executionTimeout": 600000 - } - ] -} \ No newline at end of file diff --git a/event-management/config-files/baguette-client-install/win.json b/event-management/config-files/baguette-client-install/win/win.json similarity index 100% rename from event-management/config-files/baguette-client-install/win.json rename to event-management/config-files/baguette-client-install/win/win.json diff --git a/event-management/config-files/baguette-client/conf/baguette-client.properties b/event-management/config-files/baguette-client/conf/baguette-client.properties index 887c148e1c0583ed0b3164d573d0254dc2906a9c..dded9204152a395d3efaf9e9a1472708d0afd321 100644 --- a/event-management/config-files/baguette-client/conf/baguette-client.properties +++ b/event-management/config-files/baguette-client/conf/baguette-client.properties @@ -32,6 +32,17 @@ 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 # ----------------------------------------------------------------------------- @@ -41,8 +52,11 @@ server-password = ${BAGUETTE_SERVER_PASSWORD} 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.error-limit = 3 +collector.netdata.pause-period = 60 # ----------------------------------------------------------------------------- # Cluster settings diff --git a/event-management/config-files/baguette-client/conf/baguette.json b/event-management/config-files/baguette-client/conf/baguette.json new file mode 100644 index 0000000000000000000000000000000000000000..cdd4ab4aa6c12b51c21128fba64dfb86e5db0dff --- /dev/null +++ b/event-management/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": "/opt/baguette-client/bin/kill.sh", + "waitBefore": 0, + "waitAfter": 2000 +}, { + "name": "Sending baguette client start command...", + "command": "/opt/baguette-client/bin/run.sh", + "waitBefore": 0, + "waitAfter": 10000 +}] diff --git a/event-management/config-files/baguette-client/conf/eu.melodic.event.brokercep.properties b/event-management/config-files/baguette-client/conf/eu.melodic.event.brokercep.properties index 6297e2d2fa070c49245c342869e0811a0cb5ddd4..9480923825b872d51bc672738584b622605f0a91 100644 --- a/event-management/config-files/baguette-client/conf/eu.melodic.event.brokercep.properties +++ b/event-management/config-files/baguette-client/conf/eu.melodic.event.brokercep.properties @@ -7,7 +7,9 @@ # https://www.mozilla.org/en-US/MPL/2.0/ # -password-encoder-class = eu.melodic.event.util.password.IdentityPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.AsterisksPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.IdentityPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.PresentPasswordEncoder # Broker ports and protocol brokercep.broker-name = broker diff --git a/event-management/config-files/baguette-client/conf/logback-spring.xml b/event-management/config-files/baguette-client/conf/logback-spring.xml index b2dd3dcb43be7f00a6af39eac83c533e2505246e..9ee8513aefc183641ec806daa02c8124ced5a0fa 100644 --- a/event-management/config-files/baguette-client/conf/logback-spring.xml +++ b/event-management/config-files/baguette-client/conf/logback-spring.xml @@ -32,7 +32,7 @@ - + diff --git a/event-management/config-files/baguette-client/conf/netdata.json b/event-management/config-files/baguette-client/conf/netdata.json new file mode 100644 index 0000000000000000000000000000000000000000..ed40f8260940dd4eeffdfcbd4266f8fcf8c61de2 --- /dev/null +++ b/event-management/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/event-management/config-files/eu.melodic.event.baguette-client-install.properties b/event-management/config-files/eu.melodic.event.baguette-client-install.properties index 7bb8756e891b2ba47d37f38a0429c9296fd3b28c..e7cac2b81222cb66ce6aaefb431f8b723b4cff94 100644 --- a/event-management/config-files/eu.melodic.event.baguette-client-install.properties +++ b/event-management/config-files/eu.melodic.event.baguette-client-install.properties @@ -48,11 +48,42 @@ baguette.client.install.keepTempFiles=false #baguette.client.install.command-execution-timeout = 60000 baguette.client.install.instructions.LINUX = \ - file:${MELODIC_CONFIG_DIR}/baguette-client-install/netdata.json, \ - file:${MELODIC_CONFIG_DIR}/baguette-client-install/jre8.json, \ - file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux.json, \ - file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux-start.json -baguette.client.install.instructions.WINDOWS = file:${MELODIC_CONFIG_DIR}/baguette-client-install/win.json + file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/check-ignore.json, \ + file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/detect.json, \ + file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/netdata.json, \ + file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/jre8.json, \ + file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/baguette.json, \ + file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/baguette-skip.json, \ + file:${MELODIC_CONFIG_DIR}/baguette-client-install/linux/start-agents.json +baguette.client.install.instructions.WINDOWS = file:${MELODIC_CONFIG_DIR}/baguette-client-install/win/win.json baguette.client.install.continueOnFail = true baguette.client.install.sessionRecordingDir = ${LOGS_DIR:${MELODIC_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 \ No newline at end of file diff --git a/event-management/config-files/eu.melodic.event.baguette-server.properties b/event-management/config-files/eu.melodic.event.baguette-server.properties index 44726630ed7e4193c0d4a1a5fb87c8d2e66e98cf..d8b999dae78d4ebece49927679cb3788c6ef777d 100644 --- a/event-management/config-files/eu.melodic.event.baguette-server.properties +++ b/event-management/config-files/eu.melodic.event.baguette-server.properties @@ -26,6 +26,13 @@ baguette.server.coordinatorConfig.clustering.coordinatorClass = eu.melodic.event baguette.server.coordinatorConfig.clustering.parameters.zone-management-strategy-class = eu.melodic.event.baguette.server.coordinator.cluster.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_${TIMESTAMP}_${ZONE_ID}.p12 +#baguette.server.coordinatorConfig.clustering.parameters.cluster-detector-class = eu.melodic.event.baguette.server.coordinator.cluster.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 = eu.melodic.event.baguette.server.coordinator.TwoLevelCoordinator baguette.server.coordinatorConfig.noop.coordinatorClass = eu.melodic.event.baguette.server.coordinator.NoopCoordinator @@ -45,4 +52,4 @@ baguette.server.credentials.bb=yy #baguette.server.debug.client-address-override-allowed=true baguette.server.client-id-format.escape = ~ #baguette.server.client-id-format = ~{type}-~{operatingSystem}-~{id}-~{name}-~{providerId}-~{ip}-~{random} -baguette.server.client-id-format = ~{type}-~{operatingSystem}-~{id}-~{name}-~{provider}-~{address}-~{random} +baguette.server.client-id-format = ~{type:-_}-~{operatingSystem:-_}-~{id:-_}-~{name:-_}-~{provider:-_}-~{address:-_}-~{random:-_} diff --git a/event-management/config-files/eu.melodic.event.control.properties b/event-management/config-files/eu.melodic.event.control.properties index 0a619cff13129cb33f5b585f3d371dc486fdee13..f69d2abd60c55fa284e611176fbfcdb48b7a0c70 100644 --- a/event-management/config-files/eu.melodic.event.control.properties +++ b/event-management/config-files/eu.melodic.event.control.properties @@ -8,12 +8,8 @@ # ### URLs of Upperware services being invoked by EMS -#control.esb-url = http://localhost:8088/camelModelProcessed -#control.metasolver-configuration-url = http://localhost:8092/updateConfiguration - -### Log settings -password-encoder-class = eu.melodic.event.util.password.IdentityPasswordEncoder -control.log-requests = false +#control.esb-url = ${ESB_URL:https://mule:8088} +#control.metasolver-configuration-url = ${METASOLVER_URL:http://metasolver:8092/updateConfiguration} ### Don't touch the next lines!! #IP_SETTING=%{DEFAULT_IP}% @@ -21,12 +17,22 @@ IP_SETTING=${EMS_IP_SETTING:%{PUBLIC_IP}%} #EXECUTIONWARE=CLOUDIATOR EXECUTIONWARE=PROACTIVE +################################################################################ +### Log settings +#password-encoder-class = eu.melodic.event.util.password.AsterisksPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.IdentityPasswordEncoder +#password-encoder-class = eu.melodic.event.util.password.PresentPasswordEncoder + +#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 @@ -36,34 +42,40 @@ control.tc-load-file = ${EMS_TC_FILE:${LOGS_DIR:${MELODIC_CONFIG_DIR}/../logs}/_ control.tc-save-file = ${EMS_TC_FILE:${LOGS_DIR:${MELODIC_CONFIG_DIR}/../logs}/_TC.json} ### Debug settings - Enable event debugging -control.event-debug-enabled = true +control.event-debug-enabled = ${EVENT_DEBUG_ENABLED:true} ################################################################################ ### Process CAMEL model on start-up -#control.preload.camel-model = /camel-new -#control.preload.camel-model = /mytest5 -#control.preload.camel-model = /older/TrafficSimulationUF -#control.preload.camel-model = /CRM -#control.preload.camel-model = /TrafficSimulationUF -###########control.preload.camel-model = /FCRnew control.preload.camel-model = ${EMS_PRELOAD_CAMEL_MODEL:} ### Use CP model on start-up -#control.preload.cp-model = /cpModelTest -###########control.preload.cp-model = /FCRnew_cpModelTest control.preload.cp-model = ${EMS_PRELOAD_CP_MODEL:} +################################################################################ +### 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 + + ################################################################################ ### Web configuration - Port server.port = 8111 ### Web configuration - HTTPS -#server.ssl.enabled=false -#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.enabled=true +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} #security.require-ssl=true @@ -97,23 +109,24 @@ control.ssl.key-entry-ext-san = dns:localhost,ip:127.0.0.1,ip:%{DEFAULT_IP}%,ip: #control.ssl.public-ip-address = ### Enable JWT Web security ### -melodic.security.enabled=false +#melodic.security.enabled=false +#web.jwt-token-authentication.enabled=false ### Web configuration - API key -#web.api-key.value=generate +#web.api-key.value=${random.uuid} #web.api-key.value=1234567890 #web.api-key.header=EMS-API-KEY #web.api-key.parameter=ems-api-key ### Web configuration - Resources -#static.favicon.path=classpath:/public/favicon.ico +spring.mvc.favicon.enabled=false static.favicon.path=file:${PUBLIC_DIR}/favicon.ico -#static.resource.path=file:resources/ -#static.resource.path=file:${MELODIC_CONFIG_DIR}/resources/ -#static.resource.path=file:/opt/ems-server/resources/ +#static.resource.path=file:${PUBLIC_DIR}/ +#static.resource.context=/resources/** +#static.resource.redirect=/resources/index.html static.resource.path=file:${PUBLIC_DIR}/ -static.resource.context=/resources/** -static.resource.redirect=/resources/index.html +static.resource.context=/** +static.resource.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' } static.logs.path=file:${LOGS_DIR}/ static.logs.context=/logs/** @@ -145,29 +158,33 @@ beacon.topics.threshold = _ui_threshold_info beacon.topics.instance = _ui_instance_info beacon.topics.prediction = metrics_to_predict beacon.topics.prediction.rate = 60000 -beacon.topics.slo-violator = _slo_violator_info +beacon.topics.slo-violation-detector = metric.metric_list ################################################################################ -### Hawtio web console settings +### Management and Endpoint settings #management.endpoints.web.exposure.include=hawtio,jolokia -#management.endpoints.web.base-path=/mgnt -#management.endpoints.web.path-mapping.hawtio=hawtio/console #management.endpoint.health.show-details=always +#management.endpoints.web.base-path=/mgnt +#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 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.sensitive = false #endpoints.jolokia.enabled=true #endpoints.jolokia.path=/jolokia #spring.jmx.enabled=true #endpoints.jmx.enabled=true -#management.port=9001 -#management.address=127.0.0.1 ################################################################################ ### Spring Boot Admin Client settings diff --git a/event-management/config-files/logback-conf/logback-spring.xml b/event-management/config-files/logback-conf/logback-spring.xml index 59315caa447ae04c723304c4bef691853897f6c0..d3dd2750b89af00d2ca3ba705d6e31206c538763 100644 --- a/event-management/config-files/logback-conf/logback-spring.xml +++ b/event-management/config-files/logback-conf/logback-spring.xml @@ -48,13 +48,15 @@ - - - - - + + + + + + + diff --git a/event-management/control-service/pom.xml b/event-management/control-service/pom.xml index 138d00a521f4951f358eafdd60c5b4b41654be42..f3ad9d0bbcb3ef873727dc79e03cfa649b051038 100644 --- a/event-management/control-service/pom.xml +++ b/event-management/control-service/pom.xml @@ -23,15 +23,32 @@ 2.4.0 2.3.1 2.1.0 + 1.3.1 ${maven.build.timestamp} yyyy-MM-dd HH:mm:ss + ems-server esper-${esper-version}.jar + + + + com.google.code.findbugs + jsr305 + 3.0.2 + compile + + eu.melodic @@ -70,6 +87,11 @@ broker-cep ${project.version} + + eu.melodic.event + common + ${project.version} + eu.melodic.event translator @@ -94,19 +116,34 @@ 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 @@ -153,6 +190,33 @@ + + + maven-clean-plugin + 3.1.0 + + + remove-old-public-resources + clean + + clean + + + + + + + + ${project.parent.basedir}/public_resources + + **/* + + false + + + + + pl.project13.maven git-commit-id-plugin @@ -275,42 +339,48 @@ ${project.basedir}/src/main/resources/public/index.html - ../public_resources/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/broker-client.jar + ../public_resources/resources/broker-client.jar ../bin/client.sh - ../public_resources/client.sh + ../public_resources/resources/client.sh ../baguette-client/target/baguette-client-installation-package.tgz - ../public_resources/baguette-client.tgz + ../public_resources/resources/baguette-client.tgz ../baguette-client/target/baguette-client-installation-package.tgz.md5 - ../public_resources/baguette-client.tgz.md5 + ../public_resources/resources/baguette-client.tgz.md5 ../baguette-client/bin/install.sh - ../public_resources/install.sh + ../public_resources/resources/install.sh true @@ -320,6 +390,31 @@ + + + maven-resources-plugin + 3.2.0 + + + copy-web-admin-resources + + generate-resources + + copy-resources + + + ${project.parent.basedir}/public_resources/admin + + + ${project.parent.basedir}/web-admin/dist + false + + + + + + + com.spotify docker-maven-plugin @@ -335,6 +430,8 @@ /bin ${project.basedir}/../bin/ run.sh + sysmon.sh + detect.sh /config @@ -372,6 +469,7 @@ + org.codehaus.mojo buildnumber-maven-plugin diff --git a/event-management/control-service/src/main/docker/Dockerfile b/event-management/control-service/src/main/docker/Dockerfile index 81885f588901ad9962627dbea527f2039b29b76c..c601ff096cb746b2be30fd36ed7848ff778e64f7 100644 --- a/event-management/control-service/src/main/docker/Dockerfile +++ b/event-management/control-service/src/main/docker/Dockerfile @@ -27,6 +27,10 @@ RUN apt-get update && apt-get install -y \ iputils-ping \ && rm -rf /var/lib/apt/lists/* +# XXX: TODO: Remove after tests and update url at 'config-files/baguette-client-install/jre8.json' +RUN mkdir -p ${PUBLIC_DIR}/resources +RUN curl https://cdn.azul.com/zulu/bin/zulu8.52.0.23-ca-jre8.0.282-linux_x64.tar.gz --output ${PUBLIC_DIR}/resources/zulu8.52.0.23-ca-jre8.0.282-linux_x64.tar.gz + ADD bin ${BIN_DIR} ADD jars ${JARS_DIR} ADD config ${CONFIG_DIR} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/ApplicationContext.java b/event-management/control-service/src/main/java/eu/melodic/event/control/ApplicationContext.java index 78acd8a13be1490f9b1d161775e811e002886d18..9a9a777458519c75ab18f1a3453978f739102f64 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/ApplicationContext.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/ApplicationContext.java @@ -9,12 +9,19 @@ package eu.melodic.event.control; +import eu.melodic.event.util.EventBus; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +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.client.RestTemplate; +@Slf4j @Configuration @AllArgsConstructor(onConstructor = @__({@Autowired})) public class ApplicationContext { @@ -22,4 +29,19 @@ public class ApplicationContext { public RestTemplate getRestTemplate() { return new RestTemplate(); } + + @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.info("ApplicationContext: taskScheduler: NEW INSTANCE CREATED: {}", taskExecutor); + return taskExecutor; + } } diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceApplication.java b/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceApplication.java index 923581db57afd825937608a229ba21dbf7d15ec0..c993f9db0345bcd8b3b60595acb590d79908e820 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceApplication.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceApplication.java @@ -12,10 +12,8 @@ package eu.melodic.event.control; import eu.melodic.event.control.properties.ControlServiceProperties; import eu.melodic.event.util.KeystoreUtil; import eu.melodic.event.util.PasswordUtil; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.Connector; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.Banner; import org.springframework.boot.ExitCodeGenerator; @@ -24,33 +22,25 @@ 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.context.ApplicationPidFileWriter; -import org.springframework.boot.info.BuildProperties; -import org.springframework.boot.info.InfoProperties; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.util.StreamUtils; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.stream.StreamSupport; +import java.util.Timer; +import java.util.TimerTask; @SpringBootApplication( scanBasePackages = {"eu.melodic.event.baguette.server", "eu.melodic.event.baguette.client.install", - "eu.melodic.event.brokercep", "eu.melodic.event.control", "eu.melodic.event.translate", - "eu.melodic.event.util"}, + "eu.melodic.event.baguette.client.selfhealing", "eu.melodic.event.brokercep", "eu.melodic.event.control", + "eu.melodic.event.translate", "eu.melodic.event.common", "eu.melodic.event.util"}, exclude = { SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class } ) @EnableAsync @Configuration @Slf4j -public class ControlServiceApplication implements ApplicationContextAware { +public class ControlServiceApplication /*implements ApplicationContextAware*/ { private static ConfigurableApplicationContext applicationContext; private static Timer exitTimer; @@ -58,8 +48,6 @@ public class ControlServiceApplication implements ApplicationContextAware { private ControlServiceProperties properties; @Autowired private PasswordUtil passwordUtil; - @Autowired - private BuildProperties buildProperties;; public static void main(String[] args) { // Start EMS server @@ -117,36 +105,4 @@ public class ControlServiceApplication implements ApplicationContextAware { log.warn("ControlServiceApplication.exitApp(): Exit timer has already started: {}", exitTimer); } } - - @Override - @SneakyThrows - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - if (!properties.isPrintBuildInfo()) return; - if (!log.isInfoEnabled()) return; - - // Print build info from 'BuildProperties' - log.info("--------------------------------------------------------------------------------"); - log.info("Build Info:"); - StreamSupport.stream(Spliterators.spliteratorUnknownSize(buildProperties.iterator(), Spliterator.ORDERED), false) - .sorted(Comparator.comparing(InfoProperties.Entry::getKey)) - .forEach(e->log.info(" - {} = {}", e.getKey(), e.getValue())); - log.info("--------------------------------------------------------------------------------"); - - // Print info from bundled files - printInfoFromFile(applicationContext, "Version Info", "classpath:/version.txt"); - log.info("--------------------------------------------------------------------------------"); - printInfoFromFile(applicationContext, "Git Info", "classpath:/git.properties"); - log.info("--------------------------------------------------------------------------------"); - printInfoFromFile(applicationContext, "Build Info", "classpath:/META-INF/build-info.properties"); - log.info("--------------------------------------------------------------------------------"); - } - - protected void printInfoFromFile(ApplicationContext applicationContext, String title, String resourceStr) throws IOException { - Resource[] resources = applicationContext.getResources(resourceStr); - if (resources.length>0) { - Resource r = resources[0]; - log.info("** {} **\nFile: {}\nURL: {}\n{}\n", title, r.getFilename(), r.getURL(), - StreamUtils.copyToString(r.getInputStream(), StandardCharsets.UTF_8)); - } - } } \ No newline at end of file diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceController.java b/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceController.java index 9d0b932d7e8efd1e8087a3517c5aa33132b568af..9c340e717109d829bbbe2db95ef2518f2b9e8506 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceController.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceController.java @@ -11,12 +11,16 @@ package eu.melodic.event.control; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import eu.melodic.event.baguette.client.install.*; +import eu.melodic.event.baguette.client.install.ClientInstallationTask; +import eu.melodic.event.baguette.client.install.ClientInstaller; import eu.melodic.event.baguette.client.install.helper.InstallationHelperFactory; -import eu.melodic.event.baguette.client.install.instruction.InstallationInstructions; import eu.melodic.event.baguette.server.BaguetteServer; import eu.melodic.event.baguette.server.NodeRegistryEntry; +import eu.melodic.event.baguette.server.properties.BaguetteServerProperties; import eu.melodic.event.control.properties.ControlServiceProperties; +import eu.melodic.event.control.webconf.WebSecurityConfig; +import eu.melodic.event.translate.TranslationContext; +import eu.melodic.event.util.CredentialsMap; import eu.melodic.event.util.NetUtil; import eu.melodic.models.commons.Watermark; import eu.melodic.models.interfaces.ems.*; @@ -29,13 +33,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.BadRequestException; import java.io.IOException; import java.lang.reflect.Type; import java.util.*; +import java.util.stream.Stream; import static java.lang.String.format; import static org.springframework.http.HttpStatus.BAD_REQUEST; @@ -47,11 +54,17 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; @AllArgsConstructor(onConstructor = @__({@Autowired})) public class ControlServiceController { + private final static String ROLES_ALLOWED_JWT_TOKEN_OR_API_KEY = + "hasAnyRole('"+WebSecurityConfig.ROLE_JWT_TOKEN+"','"+WebSecurityConfig.ROLE_API_KEY+"')"; + @Autowired private ControlServiceProperties properties; @Autowired private ControlServiceCoordinator coordinator; + @Autowired + private RequestMappingHandlerMapping mvcHandlerMapping; + // ------------------------------------------------------------------------------------------------------------ // ESB and Upperware interfacing methods // ------------------------------------------------------------------------------------------------------------ @@ -226,10 +239,34 @@ public class ControlServiceController { return constraints; } + @GetMapping(value = {"/translator/getTopLevelNodesMetricContexts/{appId}", "/translator/getTopLevelNodesMetricContexts"}) + public Collection getTopLevelNodesMetricContexts(@PathVariable("appId") Optional optAppId, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + String applicationId = optAppId.orElse(null); + log.info("ControlServiceController.getTopLevelNodesMetricContexts(): Received request: app-id={}", applicationId); + log.trace("ControlServiceController.getTopLevelNodesMetricContexts(): JWT token: {}", jwtToken); + + if (StringUtils.isBlank(applicationId)) { + applicationId = coordinator.getCurrentCamelModelId(); + log.info("ControlServiceController.getTopLevelNodesMetricContexts(): Using current application: curr-app-id={}", applicationId); + if (applicationId==null) return Collections.emptyList(); + } + + // Retrieve context metrics of the top-level DAG nodes + String camelModelId = (applicationId.startsWith("/")) ? applicationId : "/"+applicationId; + log.debug("ControlServiceController.getTopLevelNodesMetricContexts(): camelModelId: {}", camelModelId); + Set results = coordinator.getMetricContextsForPrediction(camelModelId); + log.info("ControlServiceController.getTopLevelNodesMetricContexts(): Result: {}", results); + + return results; + } + // ------------------------------------------------------------------------------------------------------------ // Broker-CEP query & control methods // ------------------------------------------------------------------------------------------------------------ + @PreAuthorize(ROLES_ALLOWED_JWT_TOKEN_OR_API_KEY) @RequestMapping(value = "/broker/credentials", method = {GET,POST}, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public HttpEntity getBrokerCredentials(@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) @@ -256,6 +293,72 @@ public class ControlServiceController { return entity; } + @PreAuthorize(ROLES_ALLOWED_JWT_TOKEN_OR_API_KEY) + @RequestMapping(value = "/baguette/ref/{ref}", method = {GET,POST}, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public HttpEntity getNodeCredentials(@PathVariable("ref") Optional optRef, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String jwtToken) + { + log.info("ControlServiceController.getNodeCredentials(): BEGIN: ref={}", optRef); + log.trace("ControlServiceController.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 (coordinator.getReference().equals(optRef.get())) { + if (coordinator.getBaguetteServer()==null || !coordinator.getBaguetteServer().isServerRunning()) { + log.warn("ControlServiceController.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.size()>0) { + username = credentials.keySet().stream().findFirst().orElse(null); + password = credentials.get(username); + } + String key = coordinator.getBaguetteServer().getServerPubkey(); + + log.debug("ControlServiceController.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("ControlServiceController.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("ControlServiceController.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("ControlServiceController.getNodeCredentials(): Response: ** Not shown because it contains credentials **"); + + return entity; + } + // ------------------------------------------------------------------------------------------------------------ // Baguette control methods // ------------------------------------------------------------------------------------------------------------ @@ -286,73 +389,80 @@ public class ControlServiceController { log.debug("ControlServiceController.baguetteRegisterNode(): Node information: map={}", nodeMap); // Register node to Baguette server - BaguetteServer baguette = coordinator.getBaguetteServer(); - String clientId = baguette.registerClient(nodeMap); + NodeRegistryEntry entry; + try { + entry = coordinator.getBaguetteServer().registerClient(nodeMap); + } catch (Exception e) { + log.error("ControlServiceController.baguetteRegisterNode(): EXCEPTION while registering node: map={}\n", nodeMap, e); + return "ERROR "+e.getMessage(); + } + + // Update client registration info with BASE_URL, IP_SETTING and CLIENT_ID + updateRegistrationInfo(request, entry); + + // Continue processing according to ExecutionWare type + String response; + log.info("ControlServiceController.baguetteRegisterNode(): ExecutionWare: {}", properties.getExecutionware()); + if (properties.getExecutionware()==ControlServiceProperties.ExecutionWare.CLOUDIATOR) { + response = getClientInstallationInstructions(entry); + } else { + response = createClientInstallationTask(entry); + } + log.info("ControlServiceController.baguetteRegisterNode(): node-id: {}", nodeId); + log.debug("ControlServiceController.baguetteRegisterNode(): node: {}, json: {}", nodeId, response); + return response; + } + + private void updateRegistrationInfo(HttpServletRequest request, NodeRegistryEntry entry) { // Get web server base URL String staticResourceContext = coordinator.getControlServiceProperties().getStaticResourceContext(); staticResourceContext = StringUtils.substringBeforeLast(staticResourceContext,"/**"); staticResourceContext = StringUtils.substringBeforeLast(staticResourceContext,"/*"); if (!staticResourceContext.startsWith("/")) staticResourceContext = "/"+staticResourceContext; - String ipSetting = coordinator.getControlServiceProperties().getIpSetting().toString(); String baseUrl = (ControlServiceProperties.IpSetting.DEFAULT_IP == coordinator.getControlServiceProperties().getIpSetting()) ? request.getScheme()+"://"+ NetUtil.getDefaultIpAddress() +":"+request.getServerPort()+staticResourceContext : request.getScheme()+"://"+ NetUtil.getPublicIpAddress() +":"+request.getServerPort()+staticResourceContext; log.debug("ControlServiceController.baguetteRegisterNode(): baseUrl={}", baseUrl); - // Create context map - Map contextMap = new HashMap<>(); - contextMap.put("BASE_URL", baseUrl); - contextMap.put("CLIENT_ID", clientId); - contextMap.put("IP_SETTING", ipSetting); - - // Continue processing according to ExecutionWare type - String response; - log.info("ControlServiceController.baguetteRegisterNode(): ExecutionWare: {}", properties.getExecutionware()); - if (properties.getExecutionware()==ControlServiceProperties.ExecutionWare.CLOUDIATOR) { - response = getClientInstallationInstructions(nodeMap, contextMap, baguette); - } else { - response = createClientInstallationTask(nodeMap, contextMap, baguette); - } + // Get IP Setting and Client ID + String ipSetting = coordinator.getControlServiceProperties().getIpSetting().toString(); + String clientId = entry.getClientId(); - log.info("ControlServiceController.baguetteRegisterNode(): node-id: {}", nodeId); - log.debug("ControlServiceController.baguetteRegisterNode(): node: {}, json: {}", nodeId, response); - return response; + // Add to context + entry.getPreregistration().put("BASE_URL", baseUrl); + entry.getPreregistration().put("CLIENT_ID", clientId); + entry.getPreregistration().put("IP_SETTING", ipSetting); } // Retained for backward compatibility with Cloudiator @SneakyThrows - public String getClientInstallationInstructions(Map nodeMap, Map contextMap, BaguetteServer baguette) throws IOException { + public String getClientInstallationInstructions(NodeRegistryEntry entry) throws IOException { // Prepare Baguette Client installation instructions for node - String nodeId = (String) nodeMap.get("id"); - String nodeOs = (String) nodeMap.get("operatingSystem"); final String CLOUDIATOR_HELPER_CLASS = "eu.melodic.event.extra.cloudiator.CloudiatorInstallationHelper"; - List list = InstallationHelperFactory.getInstance() - .createInstallationHelperBean(CLOUDIATOR_HELPER_CLASS, nodeMap) - .prepareInstallationInstructionsForOs(nodeMap, contextMap, baguette); - InstallationInstructions installationInstructions = - (list!=null && list.size()>0) ? list.get(0) : null; - if (installationInstructions==null) { - log.warn("ControlServiceController.baguetteRegisterNode(): ERROR: Unknown node OS: {}", nodeOs); + String json = InstallationHelperFactory.getInstance() + .createInstallationHelperBean(CLOUDIATOR_HELPER_CLASS, entry) + .getInstallationInstructionsForOs(entry) + .orElse(Collections.emptyList()) + .stream().findFirst() + .orElse(null); + if (json==null) { + log.warn("ControlServiceController.baguetteRegisterNode(): No instruction sets: node-map={}", entry.getPreregistration()); return null; } - log.debug("ControlServiceController.baguetteRegisterNode(): installationInstructions: {}", installationInstructions); - - // Convert 'installationInstructions' into json string - Gson gson = new Gson(); - String json = gson.toJson(installationInstructions, InstallationInstructions.class); + log.debug("ControlServiceController.baguetteRegisterNode(): instructionsSet: {}", json); - log.trace("ControlServiceController.baguetteRegisterNode(): installationInstructions: node: {}, json:\n{}", nodeId, json); + log.trace("ControlServiceController.baguetteRegisterNode(): instructionsSet: node-map={}, json:\n{}", entry.getPreregistration(), json); return json; } - public String createClientInstallationTask(Map nodeMap, Map contextMap, BaguetteServer baguette) throws Exception { + public String createClientInstallationTask(NodeRegistryEntry entry) throws Exception { //log.info("ControlServiceController.baguetteRegisterNodeForProactive(): INPUT: node-map: {}", nodeMap); ClientInstallationTask installationTask = InstallationHelperFactory.getInstance() - .createInstallationHelper(nodeMap) - .createClientInstallationTask(nodeMap, contextMap, baguette); + .createInstallationHelper(entry) + .createClientInstallationTask(entry); ClientInstaller.instance().addTask(installationTask); log.debug("ControlServiceController.baguetteRegisterNodeForProactive(): New installation-task: {}", installationTask); @@ -389,7 +499,7 @@ public class ControlServiceController { // Event Generation and Debugging methods // ------------------------------------------------------------------------------------------------------------ - @RequestMapping(value = "/event/generate-start/{clientId}/{topicName}/{interval}/{lowerValue}-{upperValue}", method = GET) + @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("ControlServiceController.startEventGeneration(): PARAMS: client={}, topic={}, interval={}, value-range=[{},{}]", clientId, topicName, interval, lowerValue, upperValue); return coordinator.eventGenerationStart(clientId, topicName, interval, lowerValue, upperValue); @@ -424,11 +534,17 @@ public class ControlServiceController { } @RequestMapping(value = "/client/command/{clientId}/{command:.+}", method = GET) - public String sendClientCommand(@PathVariable String clientId, @PathVariable String command) { - log.info("ControlServiceController.sendClientCommand(): PARAMS: client={}, command={}", clientId, command); + public String clientCommand(@PathVariable String clientId, @PathVariable String command) { + log.info("ControlServiceController.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("ControlServiceController.clusterCommand(): PARAMS: cluster={}, command={}", clusterId, command); + return coordinator.clusterCommandSend(clusterId, command); + } + // ------------------------------------------------------------------------------------------------------------ // EMS status and information query methods // ------------------------------------------------------------------------------------------------------------ @@ -472,40 +588,6 @@ public class ControlServiceController { return "{}"; } - @RequestMapping(value = "/ems/stats", method = {GET, POST}, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Map emsServerStatistics() { - log.debug("ControlServiceController.emsServerStatistics(): BEGIN"); - Map statsMap = coordinator.emsServerStatistics(); - log.debug("ControlServiceController.emsServerStatistics(): END: {}", statsMap); - return statsMap; - } - - @RequestMapping(value = "/ems/stats/overall", method = {GET, POST}, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Map emsOverallStatistics() { - log.debug("ControlServiceController.emsOverallStatistics(): BEGIN"); - Map statsMap = coordinator.emsOverallStatistics(); - log.debug("ControlServiceController.emsOverallStatistics(): END: {}", statsMap); - return statsMap; - } - - @RequestMapping(value = "/ems/stats/clear", method = {GET, POST}) - public String emsServerStatisticsClear() { - log.debug("ControlServiceController.emsServerStatisticsClear(): BEGIN"); - coordinator.emsServerStatisticsClear(); - log.debug("ControlServiceController.emsServerStatisticsClear(): END"); - return "OK"; - } - - @RequestMapping(value = "/ems/stats/overall/clear", method = {GET, POST}) - public String emsOverallStatisticsClear() { - log.debug("ControlServiceController.emsOverallStatisticsClear(): BEGIN"); - coordinator.emsOverallStatisticsClear(); - log.debug("ControlServiceController.emsOverallStatisticsClear(): END"); - return "OK"; - } - // ------------------------------------------------------------------------------------------------------------ @RequestMapping(value = "/health", method = GET) @@ -528,4 +610,20 @@ public class ControlServiceController { protected String stripQuotes(String s) { return (s != null && s.startsWith("\"") && s.endsWith("\"")) ? s.substring(1, s.length() - 1) : s; } + + public Stream getControllerEndpoints() { + return mvcHandlerMapping.getHandlerMethods().keySet().stream() + .filter(Objects::nonNull) + .map(k -> k.getPatternsCondition().getPatterns()) + .flatMap(Set::stream); + } + + public String[] getControllerEndpointsShort() { + return getControllerEndpoints() + .map(s -> s.startsWith("/") ? s.substring(1) : s) + .map(s -> s.indexOf("/") > 0 ? s.split("/", 2)[0] + "/**" : s) + .map(e -> "/" + e.replaceAll("\\{.*", "**")) + .distinct() + .toArray(String[]::new); + } } diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceCoordinator.java b/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceCoordinator.java index 9f528cdda0a897b767a0f14206e84dc531466dd4..fe3bd0e947207a5fe2d1c4d37e9ce4111aca3af0 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceCoordinator.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/ControlServiceCoordinator.java @@ -10,15 +10,21 @@ package eu.melodic.event.control; import camel.core.NamedElement; +import camel.metric.CompositeMetric; +import camel.metric.MetricContext; +import camel.metric.RawMetric; +import camel.requirement.ServiceLevelObjective; import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.baguette.server.NodeRegistry; +import eu.melodic.event.baguette.server.ServerCoordinator; import eu.melodic.event.brokercep.BrokerCepService; import eu.melodic.event.brokercep.BrokerCepStatementSubscriber; import eu.melodic.event.brokercep.event.EventMap; +import eu.melodic.event.control.collector.netdata.ServerNetdataCollector; import eu.melodic.event.control.properties.ControlServiceProperties; import eu.melodic.event.translate.CamelToEplTranslator; import eu.melodic.event.translate.TranslationContext; import eu.melodic.event.translate.analyze.DAGNode; -import eu.melodic.event.translate.analyze.Grouping; import eu.melodic.event.util.KeystoreUtil; import eu.melodic.event.util.PasswordUtil; import eu.melodic.models.commons.NotificationResult; @@ -31,6 +37,7 @@ import eu.melodic.models.interfaces.ems.Sink; import eu.melodic.models.services.ems.CamelModelNotificationRequest; import eu.melodic.models.services.ems.CamelModelNotificationRequestImpl; import lombok.Getter; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.HttpClient; @@ -38,6 +45,7 @@ import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContextBuilder; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationContext; @@ -53,6 +61,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.io.IOException; +import java.nio.file.Paths; import java.security.*; import java.security.cert.CertificateException; import java.util.*; @@ -61,7 +70,7 @@ import java.util.stream.Collectors; @Slf4j @Service -public class ControlServiceCoordinator { +public class ControlServiceCoordinator implements InitializingBean { @Autowired private ApplicationContext applicationContext; @@ -73,6 +82,8 @@ public class ControlServiceCoordinator { @Getter private BrokerCepService brokerCep; @Autowired + private NodeRegistry nodeRegistry; + @Autowired private RestTemplate restTemplate; @Autowired private PasswordUtil passwordUtil; @@ -86,6 +97,34 @@ public class ControlServiceCoordinator { private String currentCpModelId; private TranslationContext currentTC; + private ServerNetdataCollector netdataCollector; + + @Getter + private String reference = UUID.randomUUID().toString(); + + 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 { + // 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 has been set. Check property: control.tc-load-file"); + if (! Paths.get(properties.getTcLoadFile()).toFile().exists()) + throw new IllegalArgumentException("Model translation will be skipped (see property control.skip-translation), but specified Translation Context file does not exist. Check property: control.tc-load-file=" + properties.getTcLoadFile()); + log.warn("Model translation will be skipped, and Translation Context file will be used: {}", properties.getTcLoadFile()); + } + } // ------------------------------------------------------------------------------------------------------------ @@ -93,6 +132,12 @@ public class ControlServiceCoordinator { return properties; } + public void setCurrentEmsState(@NonNull EMS_STATE newState, String message) { + this.currentEmsState = newState; + this.currentEmsStateMessage = message; + this.currentEmsStateChangeTimestamp = System.currentTimeMillis(); + } + // ------------------------------------------------------------------------------------------------------------ @EventListener(ApplicationReadyEvent.class) @@ -138,6 +183,8 @@ public class ControlServiceCoordinator { this.currentCamelModelId = camelModelId; this.currentCpModelId = cpModelId; } catch (Exception ex) { + setCurrentEmsState(EMS_STATE.ERROR, ex.getMessage()); + String mesg = "ControlServiceCoordinator.processNewModel(): EXCEPTION: " + ex; log.error(mesg, ex); if (!properties.isSkipEsbNotification()) { @@ -170,6 +217,8 @@ public class ControlServiceCoordinator { _processCpModel(cpModelId, notificationUri, requestUuid, jwtToken); this.currentCpModelId = cpModelId; } catch (Exception ex) { + setCurrentEmsState(EMS_STATE.ERROR, ex.getMessage()); + String mesg = "ControlServiceCoordinator.processCpModel(): EXCEPTION: " + ex; log.error(mesg, ex); if (!properties.isSkipEsbNotification()) { @@ -193,6 +242,8 @@ public class ControlServiceCoordinator { // Translate models into EPL rules etc TranslationContext _TC = null; if (!properties.isSkipTranslation()) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Retrieving and translating CAMEL model"); + log.info("ControlServiceCoordinator.processNewModel(): CAMEL-to-EPL rule translation: camel-model-id={}", camelModelId); CamelToEplTranslator translator = applicationContext.getBean(CamelToEplTranslator.class); @@ -203,6 +254,8 @@ public class ControlServiceCoordinator { String fileName = properties.getTcSaveFile(); if (StringUtils.isNotBlank(fileName)) { try { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Storing translation context to file"); + log.info("ControlServiceCoordinator.processNewModel(): Start serializing _TC data in file: {}", fileName); java.io.Writer writer = new java.io.FileWriter(fileName); com.google.gson.Gson gson = new com.google.gson.GsonBuilder().create(); @@ -247,6 +300,8 @@ public class ControlServiceCoordinator { // unserialize 'TranslationContext' from file String fileName = properties.getTcLoadFile(); if (StringUtils.isNotBlank(fileName)) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Loading translation context from file"); + try { log.info("ControlServiceCoordinator.processNewModel(): Start unserializing _TC data from file: {}", fileName); java.io.Reader reader = new java.io.FileReader(fileName); @@ -261,6 +316,8 @@ public class ControlServiceCoordinator { } catch (java.io.IOException ex) { log.error("ControlServiceCoordinator.processNewModel(): FAILED to unserialize _TC from file: {} : Exception: ", fileName, ex); } + } else { + throw new IllegalArgumentException("No translation context file has been set"); } } @@ -268,6 +325,8 @@ public class ControlServiceCoordinator { Map constants = new HashMap<>(); if (!properties.isSkipMvvRetrieve()) { if (StringUtils.isNotBlank(cpModelId)) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Retrieving MVVs from CP model"); + try { log.info("ControlServiceCoordinator.processNewModel(): Retrieving MVVs from CP model: cp-model-id={}", cpModelId); @@ -290,6 +349,8 @@ public class ControlServiceCoordinator { // (Re-)Configure Broker and CEP String upperwareGrouping = properties.getUpperwareGrouping(); if (!properties.isSkipBrokerCep()) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "initializing Broker-CEP"); + try { // Initializing Broker-CEP module if necessary if (brokerCep == null) { @@ -352,6 +413,8 @@ public class ControlServiceCoordinator { // (Re-)Configure Baguette server if (!properties.isSkipBaguette()) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Initializing Baguette Server"); + log.info("ControlServiceCoordinator.processNewModel(): Re-configuring Baguette Server: camel-model-id={}", camelModelId); try { baguette.setTopologyConfiguration(_TC, constants, upperwareGrouping, brokerCep); @@ -362,8 +425,38 @@ public class ControlServiceCoordinator { log.warn("ControlServiceCoordinator.processNewModel(): Skipping Baguette Server setup due to configuration"); } + // Start/Stop Top-Level collectors + if (!properties.isSkipCollectors()) { + if (netdataCollector!=null) { + log.info("ControlServiceCoordinator.processNewModel(): Stopping NetdataCollector: camel-model-id={}", camelModelId); + try { + netdataCollector.stop(); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.processNewModel(): EXCEPTION while stopping NetdataCollector: camel-model-id={}", camelModelId, ex); + } + } + ServerCoordinator serverCoordinator = nodeRegistry.getCoordinator(); + if (! serverCoordinator.supportsAggregators()) { + if (netdataCollector==null) { + netdataCollector = applicationContext.getBean(ServerNetdataCollector.class); + } + log.info("ControlServiceCoordinator.processNewModel(): Starting NetdataCollector: camel-model-id={}", camelModelId); + try { + netdataCollector.start(); + } catch (Exception ex) { + log.error("ControlServiceCoordinator.processNewModel(): EXCEPTION while starting NetdataCollector: camel-model-id={}", camelModelId, ex); + } + } else { + log.info("ControlServiceCoordinator.processNewModel(): NetdataCollector is not needed (will not start it): camel-model-id={}", camelModelId); + } + } else { + log.warn("ControlServiceCoordinator.processNewModel(): Skipping Collectors setup due to configuration"); + } + // (Re-)Configure MetaSolver if (!properties.isSkipMetasolver()) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Sending configuration to MetaSolver"); + // Get scaling event and SLO topics from _TC Set scalingTopics = new HashSet<>(); scalingTopics.addAll(_TC.E2A.keySet()); @@ -435,6 +528,8 @@ public class ControlServiceCoordinator { // Notify ESB, if 'notificationUri' is provided if (!properties.isSkipEsbNotification()) { if (StringUtils.isNotBlank(notificationUri)) { + setCurrentEmsState(EMS_STATE.INITIALIZING, "Notifying ESB"); + notificationUri = notificationUri.trim(); log.info("ControlServiceCoordinator.processNewModel(): Notifying ESB: {}", notificationUri); sendSuccessNotification(camelModelId, notificationUri, requestUuid, jwtToken); @@ -446,6 +541,8 @@ public class ControlServiceCoordinator { this.currentTC = _TC; log.info("ControlServiceCoordinator.processNewModel(): END: camel-model-id={}", camelModelId); + + setCurrentEmsState(EMS_STATE.READY, null); } protected void _processCpModel(String cpModelId, String notificationUri, String requestUuid, String jwtToken) { @@ -457,6 +554,8 @@ public class ControlServiceCoordinator { Map constants = new HashMap<>(); if (!properties.isSkipMvvRetrieve()) { if (StringUtils.isNotBlank(cpModelId)) { + setCurrentEmsState(EMS_STATE.RECONFIGURING, "Retrieving MVVs from CP model"); + try { log.info("ControlServiceCoordinator._processCpModel(): Retrieving MVVs from CP model: cp-model-id={}", cpModelId); @@ -480,6 +579,8 @@ public class ControlServiceCoordinator { // (Re-)Configure Broker and CEP if (!properties.isSkipBrokerCep()) { try { + setCurrentEmsState(EMS_STATE.RECONFIGURING, "Reconfiguring Broker-CEP"); + // Initializing Broker-CEP module if necessary if (brokerCep == null) { log.info("ControlServiceCoordinator._processCpModel(): Broker-CEP: Initializing..."); @@ -498,6 +599,8 @@ public class ControlServiceCoordinator { // (Re-)Configure Baguette server if (!properties.isSkipBaguette()) { + setCurrentEmsState(EMS_STATE.RECONFIGURING, "Reconfiguring Baguette Server"); + log.info("ControlServiceCoordinator._processCpModel(): Re-configuring Baguette Server with constants: {}", constants); try { baguette.sendConstants(constants); @@ -511,6 +614,8 @@ public class ControlServiceCoordinator { // Notify ESB, if 'notificationUri' is provided if (!properties.isSkipEsbNotification()) { if (StringUtils.isNotBlank(notificationUri)) { + setCurrentEmsState(EMS_STATE.RECONFIGURING, "Notifying ESB"); + notificationUri = notificationUri.trim(); log.info("ControlServiceCoordinator._processCpModel(): Notifying ESB: {}", notificationUri); sendSuccessNotification(null, notificationUri, requestUuid, jwtToken); @@ -521,6 +626,8 @@ public class ControlServiceCoordinator { } log.info("ControlServiceCoordinator._processCpModel(): END: cp-model-id={}", cpModelId); + + setCurrentEmsState(EMS_STATE.READY, null); } // ------------------------------------------------------------------------------------------------------------ @@ -558,7 +665,7 @@ public class ControlServiceCoordinator { return _tc.getMetricConstraints(); } - public Set getGlobalGroupingMetrics(String camelModelId) { + /*public Set getGlobalGroupingMetrics(String camelModelId) { TranslationContext _tc = camelToTcCache.get(camelModelId); if (_tc==null) return Collections.emptySet(); @@ -577,9 +684,18 @@ public class ControlServiceCoordinator { return nodes.stream() .map(DAGNode::getElementName) .collect(Collectors.toSet()); + }*/ + + public @NonNull Map getSLOMetricDecomposition(String camelModelId) { + List slos = _getSLOMetricDecomposition(camelModelId); + Map result = new HashMap<>(); + result.put("name", "_"); + result.put("operator", "OR"); + result.put("constraints", slos); + return result; } - public List getSLOMetricDecomposition(String camelModelId) { + public @NonNull List _getSLOMetricDecomposition(String camelModelId) { TranslationContext _tc = camelToTcCache.get(camelModelId); if (_tc==null) return Collections.emptyList(); @@ -588,6 +704,8 @@ public class ControlServiceCoordinator { .collect(Collectors.toMap(TranslationContext.MetricConstraint::getName, mc -> mc)); Map lcMap = _tc.getLogicalConstraints().stream() .collect(Collectors.toMap(TranslationContext.LogicalConstraint::getName, lc -> lc)); + /*Map ifMap = _tc.getIfThenConstraints().stream() + .collect(Collectors.toMap(TranslationContext.IfThenConstraint::getName, ic -> ic));*/ // Create map of top-level element names and instances Set topLevelNodes = _tc.DAG.getTopLevelNodes(); @@ -642,6 +760,39 @@ public class ControlServiceCoordinator { return null; } + public @NonNull Set getMetricContextsForPrediction(String camelModelId) { + log.debug("getMetricContextsForPrediction: BEGIN: {}", camelModelId); + TranslationContext _tc = camelToTcCache.get(camelModelId); + if (_tc==null) { + log.debug("getMetricContextsForPrediction: END: No Translation Context found for model: {}", camelModelId); + return Collections.emptySet(); + } + + // Process DAG top-level nodes + Set topLevelNodes = _tc.DAG.getTopLevelNodes(); + HashSet tcMetricsOfTopLevelNodes = new HashSet<>(); + + final Deque q = topLevelNodes.stream() + .filter(x -> + x.getElement() instanceof ServiceLevelObjective || + x.getElement() instanceof CompositeMetric || + x.getElement() instanceof RawMetric) + .distinct() + .collect(Collectors.toCollection(ArrayDeque::new)); + + while (!q.isEmpty()) { + DAGNode node = q.pop(); + if (node.getElement() instanceof MetricContext) { + tcMetricsOfTopLevelNodes.add(node.getMetricContext()); + } else { + Set children = _tc.DAG.getNodeChildren(node); + if (children!=null) q.addAll(children); + } + } + + return tcMetricsOfTopLevelNodes; + } + // ------------------------------------------------------------------------------------------------------------ // Baguette control methods @@ -943,12 +1094,22 @@ public class ControlServiceCoordinator { public List clientList() { log.debug("ControlServiceCoordinator.clientList(): BEGIN:"); - return baguette.getActiveClients(); + return baguette.isServerRunning() ? baguette.getActiveClients() : Collections.emptyList(); } public Map> clientMap() { log.debug("ControlServiceCoordinator.clientMap(): BEGIN:"); - return baguette.getActiveClientsMap(); + return baguette.isServerRunning() ? baguette.getActiveClientsMap() : Collections.emptyMap(); + } + + public List passiveClientList() { + log.debug("ControlServiceCoordinator.passiveClientList(): BEGIN:"); + return baguette.isServerRunning() ? baguette.getPassiveNodes() : Collections.emptyList(); + } + + public Map> passiveClientMap() { + log.debug("ControlServiceCoordinator.passiveClientMap(): BEGIN:"); + return baguette.isServerRunning() ? baguette.getPassiveNodesMap() : Collections.emptyMap(); } public String clientCommandSend(String clientId, String command) { @@ -956,51 +1117,25 @@ public class ControlServiceCoordinator { return eventSendCommandToClient("clientCommandSend", clientId, command); } - // ------------------------------------------------------------------------------------------------------------ - - public Map emsServerStatistics() { - log.debug("ControlServiceCoordinator.emsServerStatistics(): BEGIN"); - Map statsMap = brokerCep.getBrokerCepStatistics(); - log.debug("ControlServiceCoordinator.emsServerStatistics(): END: {}", statsMap); - return statsMap; - } - - public Map emsOverallStatistics() { - log.debug("ControlServiceCoordinator.emsOverallStatistics(): BEGIN"); - - // Collecting EMS server statistics - Map serverStats = emsServerStatistics(); - Map statsMap = new HashMap<>(); - statsMap.put("server", serverStats); - - // Collecting EMS clients' statistics - log.trace("ControlServiceCoordinator.emsOverallStatistics(): clients: {}", clientList()); - for (String clientId : clientList().stream().map(s->s.split(" ")[0]).collect(Collectors.toList())) { - log.trace("ControlServiceCoordinator.emsOverallStatistics(): Requesting statistics from client: {}", clientId); - Object o = baguette.readFromClient(clientId, "SHOW-STATS"); - log.trace("ControlServiceCoordinator.emsOverallStatistics(): Statistics from client: {}, stats: {}", clientId, o); - if (o instanceof Map) { - statsMap.put(clientId, o); - } - } - - log.debug("ControlServiceCoordinator.emsOverallStatistics(): END: {}", statsMap); - return statsMap; + public String clusterCommandSend(String clusterId, String command) { + log.debug("ControlServiceCoordinator.clusterCommandSend(): BEGIN: cluster={}, command={}", clusterId, command); + return sendCommandToCluster("clusterCommandSend", clusterId, command); } - public void emsServerStatisticsClear() { - log.debug("ControlServiceCoordinator.emsServerStatisticsClear(): BEGIN"); - brokerCep.clearBrokerCepStatistics(); - log.info("ControlServiceCoordinator.emsServerStatisticsClear(): EMS server statistics cleared"); - log.debug("ControlServiceCoordinator.emsServerStatisticsClear(): END"); - } + private String sendCommandToCluster(String method, String clusterId, String command) { + // Check status + if (!properties.isEventDebugEnabled()) return eventLogEnd(method, EVENT_DEBUG_DISABLED); + if (properties.isSkipBaguette()) return eventLogEnd(method, BAGUETTE_DISABLED); + if (!baguette.isServerRunning()) return eventLogEnd(method, BAGUETTE_NOT_RUNNING); - public void emsOverallStatisticsClear() { - log.debug("ControlServiceCoordinator.emsOverallStatisticsClear(): BEGIN"); - emsServerStatisticsClear(); - clientCommandSend("*", "CLEAR-STATS"); - log.info("ControlServiceCoordinator.emsOverallStatisticsClear(): All EMS statistics cleared"); - log.debug("ControlServiceCoordinator.emsOverallStatisticsClear(): END"); + // Send command + if ("*".equals(clusterId)) + baguette.sendToActiveClusters(command); + else + baguette.sendToCluster(clusterId, command); + + // Log success + return eventLogEnd(method, EVENT_DEBUG_OK); } // ------------------------------------------------------------------------------------------------------------ diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/ThreadConfig.java b/event-management/control-service/src/main/java/eu/melodic/event/control/ThreadConfig.java index 10431248c41f93e017d0e5abfddc778955355081..311860431b7fdaf7c324f97bab6c7b02a2babae3 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/ThreadConfig.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/ThreadConfig.java @@ -16,9 +16,9 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -@Configuration -@EnableAsync @Slf4j +@EnableAsync +@Configuration public class ThreadConfig { /*@Bean diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/collector/Collector.java b/event-management/control-service/src/main/java/eu/melodic/event/control/collector/Collector.java new file mode 100644 index 0000000000000000000000000000000000000000..8a4b28282013ee0af092b09f496b2c70ff334314 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/collector/Collector.java @@ -0,0 +1,15 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.collector; + +import eu.melodic.event.util.Plugin; + +public interface Collector extends Plugin { +} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/collector/ServerCollectorContext.java b/event-management/control-service/src/main/java/eu/melodic/event/control/collector/ServerCollectorContext.java new file mode 100644 index 0000000000000000000000000000000000000000..d845cf23e3c17f3649765f3d7d133960ba9da9ea --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/collector/ServerCollectorContext.java @@ -0,0 +1,67 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.collector; + +import eu.melodic.event.baguette.server.NodeRegistry; +import eu.melodic.event.baguette.server.NodeRegistryEntry; +import eu.melodic.event.brokercep.BrokerCepService; +import eu.melodic.event.brokercep.event.EventMap; +import eu.melodic.event.common.collector.CollectorContext; +import eu.melodic.event.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() { + 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 boolean sendEvent(String connectionString, String destinationName, EventMap event, boolean createDestination) { + assert(connectionString==null); + if (createDestination || brokerCepService.destinationExists(destinationName)) { + brokerCepService.publishEvent(null, destinationName, event); + return true; + } + return false; + } +} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/collector/netdata/ServerNetdataCollector.java b/event-management/control-service/src/main/java/eu/melodic/event/control/collector/netdata/ServerNetdataCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..0933238fd6d79439e6395f2a5beac6dbdb3ccf72 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/collector/netdata/ServerNetdataCollector.java @@ -0,0 +1,37 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.collector.netdata; + +import eu.melodic.event.common.collector.CollectorContext; +import eu.melodic.event.common.collector.netdata.NetdataCollectorProperties; +import eu.melodic.event.control.collector.Collector; +import eu.melodic.event.control.collector.ServerCollectorContext; +import eu.melodic.event.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 eu.melodic.event.common.collector.netdata.NetdataCollector implements Collector { + public ServerNetdataCollector(@NonNull NetdataCollectorProperties properties, + @NonNull CollectorContext collectorContext, + @NonNull TaskScheduler taskScheduler, + @NonNull EventBus eventBus) + { + super(properties, collectorContext, taskScheduler, eventBus); + if (!(collectorContext instanceof ServerCollectorContext)) + throw new IllegalArgumentException("Invalid CollectorContext provided. Expected: ServerCollectorContext, but got "+collectorContext.getClass().getName()); + } +} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/info/BuildInfoProvider.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/BuildInfoProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..e8b3ac592caf61959558ff05961f36db5d49f8d3 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/BuildInfoProvider.java @@ -0,0 +1,103 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.info; + +import eu.melodic.event.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.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +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/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceBuildInfoEndpoint.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceBuildInfoEndpoint.java new file mode 100644 index 0000000000000000000000000000000000000000..2559ce366ceda9a977a1de5b70bbc8cd82b3f40e --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceBuildInfoEndpoint.java @@ -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/ + */ + +package eu.melodic.event.control.info; + +import io.micrometer.core.lang.NonNullApi; +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 +@NonNullApi +@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/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceHealthIndicator.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceHealthIndicator.java new file mode 100644 index 0000000000000000000000000000000000000000..98dff8c80ee5a66e2a5c7987933449f86fcc7a48 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceHealthIndicator.java @@ -0,0 +1,36 @@ +/* + * 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/ + */ + +package eu.melodic.event.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/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceInfoEndpointExtension.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceInfoEndpointExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..dffd263ec9ec82321c6f8c8ab61fba1ce36ff5ae --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceInfoEndpointExtension.java @@ -0,0 +1,40 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.info; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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) +public class ControlServiceInfoEndpointExtension { + + private final ApplicationContext applicationContext; + private final InfoEndpoint delegate; + + @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/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceLiveInfoEndpoint.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceLiveInfoEndpoint.java new file mode 100644 index 0000000000000000000000000000000000000000..4d2facb64344bd75d9019ccd0038d0543bb30343 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceLiveInfoEndpoint.java @@ -0,0 +1,41 @@ +/* + * 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/ + */ + +package eu.melodic.event.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/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceMBean.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceMBean.java new file mode 100644 index 0000000000000000000000000000000000000000..573afe175305bb92258bd442fb8de905e11728a8 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceMBean.java @@ -0,0 +1,95 @@ +/* + * 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/ + */ + +package eu.melodic.event.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 = "eu.melodic.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/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceMetrics.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceMetrics.java new file mode 100644 index 0000000000000000000000000000000000000000..973f4c4d6b765be3d743e0e38609e80d0782e5e6 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/ControlServiceMetrics.java @@ -0,0 +1,45 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.info; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.lang.NonNullApi; +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 +@NonNullApi +@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/event-management/control-service/src/main/java/eu/melodic/event/control/info/EmsInfoServiceImpl.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/EmsInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4199a670d27bc538fea8e06549c832ef621f70f4 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/EmsInfoServiceImpl.java @@ -0,0 +1,275 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.info; + +import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.brokercep.BrokerCepService; +import eu.melodic.event.control.ControlServiceCoordinator; +import eu.melodic.event.control.properties.ControlServiceProperties; +import eu.melodic.event.translate.TranslationContext; +import eu.melodic.event.util.FunctionDefinition; +import eu.melodic.event.util.GROUPING; +import eu.melodic.event.util.NetUtil; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.event.Level; +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 properties; + private final ControlServiceCoordinator controlServiceCoordinator; + private final BaguetteServer baguetteServer; + + private final BuildInfoProvider buildInfoProvider; + private final SystemInfoProvider systemInfoProvider; + private final BrokerCepService brokerCepService; + private final SystemResourceMonitor systemResourceMonitor; + + @Override + public void clearServerMetricValues() { + log.debug("clearServerMetricValues(): BEGIN"); + synchronized (currentServerMetricsVersion) { + systemInfoProvider.clearMetricValues(); + brokerCepService.clearBrokerCepStatistics(); + currentServerMetrics = null; + } + 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 (Map) getServerMetricValues().get(key); + } + + // ------------------------------------------------------------------------ + + @Override + public void clearClientMetricValues() { + log.debug("clearClientMetricValues(): BEGIN"); + synchronized (currentClientMetricsVersion) { + currentClientMetrics = null; + controlServiceCoordinator.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 (Map) 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 < properties.getMetricsUpdateInterval()) { + log.debug("updateServerMetricValues(): STOP: Retry in {}ms", + timestamp+properties.getMetricsUpdateInterval()-System.currentTimeMillis()); + return; + } + } + + long timestamp = System.currentTimeMillis(); + log.trace("updateServerMetricValues(): new-timestamp: {}", timestamp); + + Map metrics = new LinkedHashMap<>(); + + metrics.put("ip-address", + controlServiceCoordinator.getControlServiceProperties().getIpSetting()==ControlServiceProperties.IpSetting.PUBLIC_IP + ? NetUtil.getPublicIpAddress() + : NetUtil.getDefaultIpAddress()); + metrics.put("public-ip-address", NetUtil.getPublicIpAddress()); + metrics.put("default-ip-address", NetUtil.getDefaultIpAddress()); + metrics.put("reference", controlServiceCoordinator.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-camel-model-id", controlServiceCoordinator.getCurrentCamelModelId()); + controlServiceInfo.put("current-cp-model-id", controlServiceCoordinator.getCurrentCpModelId()); + if (controlServiceCoordinator.getControlServiceProperties()!=null) { + controlServiceInfo.put("prop-ip-setting", controlServiceCoordinator.getControlServiceProperties().getIpSetting()); + controlServiceInfo.put("prop-esb-url", controlServiceCoordinator.getControlServiceProperties().getEsbUrl()); + controlServiceInfo.put("prop-metasolver-config-url", controlServiceCoordinator.getControlServiceProperties().getMetasolverConfigurationUrl()); + controlServiceInfo.put("prop-metrics-update-interval", controlServiceCoordinator.getControlServiceProperties().getMetricsUpdateInterval()); + controlServiceInfo.put("prop-metrics-client-update-interval", controlServiceCoordinator.getControlServiceProperties().getMetricsClientUpdateInterval()); + controlServiceInfo.put("prop-metrics-stream-event-name", controlServiceCoordinator.getControlServiceProperties().getMetricsStreamEventName()); + controlServiceInfo.put("prop-metrics-stream-update-interval", controlServiceCoordinator.getControlServiceProperties().getMetricsStreamUpdateInterval()); + controlServiceInfo.put("prop-executionware", controlServiceCoordinator.getControlServiceProperties().getExecutionware().toString()); + controlServiceInfo.put("prop-preload-camel-model", controlServiceCoordinator.getControlServiceProperties().getPreloadCamelModel()); + controlServiceInfo.put("prop-preload-cp-model", controlServiceCoordinator.getControlServiceProperties().getPreloadCpModel()); + controlServiceInfo.put("prop-static-resource-context", controlServiceCoordinator.getControlServiceProperties().getStaticResourceContext()); + controlServiceInfo.put("prop-upperware-grouping", controlServiceCoordinator.getControlServiceProperties().getUpperwareGrouping()); + controlServiceInfo.put("prop-tc-load-file", controlServiceCoordinator.getControlServiceProperties().getTcLoadFile()); + controlServiceInfo.put("prop-tc-save-file", controlServiceCoordinator.getControlServiceProperties().getTcSaveFile()); + + Map debugFlags = new LinkedHashMap<>(); + debugFlags.put("event-debug-enabled", controlServiceCoordinator.getControlServiceProperties().isEventDebugEnabled()); + debugFlags.put("exit-allowed", controlServiceCoordinator.getControlServiceProperties().isExitAllowed()); + debugFlags.put("print-build-info", controlServiceCoordinator.getControlServiceProperties().isPrintBuildInfo()); + debugFlags.put("skip-translation", controlServiceCoordinator.getControlServiceProperties().isSkipTranslation()); + debugFlags.put("skip-broker-cep-init", controlServiceCoordinator.getControlServiceProperties().isSkipBrokerCep()); + debugFlags.put("skip-baguette-server-init", controlServiceCoordinator.getControlServiceProperties().isSkipBaguette()); + debugFlags.put("skip-mvv-retrieve", controlServiceCoordinator.getControlServiceProperties().isSkipMvvRetrieve()); + debugFlags.put("skip-metasolver-configuration", controlServiceCoordinator.getControlServiceProperties().isSkipMetasolver()); + debugFlags.put("skip-esb-notification", controlServiceCoordinator.getControlServiceProperties().isSkipEsbNotification()); + controlServiceInfo.put("prop-debug-flags",debugFlags); + } + 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", controlServiceCoordinator.clientList()); + baguetteServerInfo.put("active-clients-map", controlServiceCoordinator.clientMap()); + baguetteServerInfo.put("passive-clients-list", controlServiceCoordinator.passiveClientList()); + baguetteServerInfo.put("passive-clients-map", controlServiceCoordinator.passiveClientMap()); + 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 camelModelId = controlServiceCoordinator.getCurrentCamelModelId(); + if (StringUtils.isNotBlank(camelModelId)) { + TranslationContext _TC = controlServiceCoordinator.getTranslationContextOfCamelModel(camelModelId); + Set groupings = _TC.G2T.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("camel-model-id", camelModelId); + translatorInfo.put("groupings", orderedGroupings); + translatorInfo.put("actions-per-event", _TC.E2A); + translatorInfo.put("slo", _TC.SLO); + translatorInfo.put("monitors", _TC.MONS); + translatorInfo.put("rules-per-grouping", _TC.G2R); + translatorInfo.put("destinations-per-grouping", _TC.G2T); + translatorInfo.put("composite-metric-variables", _TC.CMVAR); + translatorInfo.put("metric-variable-values", _TC.MVV); + translatorInfo.put("metric-variable-values-for-CP", _TC.MVV_CP); + translatorInfo.put("destination-connections", _TC.getTopicConnections()); + translatorInfo.put("function-definitions", _TC.FUNC.stream() + .map(FunctionDefinition::toString).collect(Collectors.toList())); + } + + 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"); + } + } + + protected void updateClientMetricValues() { + log.debug("updateClientMetricValues(): BEGIN"); + if (currentClientMetrics!=null) { + long timestamp = (long) currentClientMetrics.get(".timestamp"); + log.trace("updateClientMetricValues(): stored-timestamp: {}", timestamp); + if (System.currentTimeMillis() - timestamp < properties.getMetricsClientUpdateInterval()) { + log.debug("updateClientMetricValues(): STOP: Retry in {}ms", + timestamp+properties.getMetricsClientUpdateInterval()-System.currentTimeMillis()); + return; + } + } + + long timestamp = System.currentTimeMillis(); + log.trace("updateClientMetricValues(): new-timestamp: {}", timestamp); + + Map clientMetrics = new LinkedHashMap<>(); + + // Collecting EMS clients' metrics + List clientIds = controlServiceCoordinator.clientList(); + log.trace("updateClientMetricValues(): active-baguette-clients: {}", clientIds); + for (String clientId : clientIds.stream().map(s->s.split(" ")[0]).collect(Collectors.toList())) { + log.trace("updateClientMetricValues(): Requesting metrics from client: {}", clientId); + Object o = baguetteServer.readFromClient(clientId, "SHOW-STATS", 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.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/event-management/control-service/src/main/java/eu/melodic/event/control/info/IEmsInfoProvider.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/IEmsInfoProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..dea7e839c213e55b621e39e85e04f949fa235740 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/IEmsInfoProvider.java @@ -0,0 +1,22 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.info; + +import java.util.Map; + +public interface IEmsInfoProvider { + default void clearMetricValues() { } + + default Map getMetricValues() { return null; } + + default Map getMetricValuesFor(String key) { + return (Map) getMetricValues().get(key); + } +} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/info/IEmsInfoService.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/IEmsInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..2fa626673db5388ee831871ab926b1d0ef564f74 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/IEmsInfoService.java @@ -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/ + */ + +package eu.melodic.event.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/event-management/control-service/src/main/java/eu/melodic/event/control/info/InfoServiceController.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/InfoServiceController.java new file mode 100644 index 0000000000000000000000000000000000000000..954b436a00fbf5896cba64d666aed91c3d3da77f --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/InfoServiceController.java @@ -0,0 +1,232 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.info; + +import eu.melodic.event.control.ControlServiceCoordinator; +import eu.melodic.event.control.properties.ControlServiceProperties; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.codec.ServerSentEvent; +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; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.QueryParam; +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class InfoServiceController { + + private final ControlServiceProperties properties; + private final ControlServiceCoordinator coordinator; + private final IEmsInfoService emsInfoService; + + @GetMapping("/info/metrics/get") + public Mono> serverMetricsGet(HttpServletRequest request) { + log.info("serverMetricsGet(): --- client: {}:{}", request.getRemoteAddr(), request.getRemotePort()); + Map message = createServerMetricsResult(null, -1L); + log.debug("serverMetricsGet(): message={}", message); + return Mono.just(message); + } + + @GetMapping("/info/metrics/stream") + public Flux>> serverMetricsStream( + @QueryParam("interval") Optional interval, HttpServletRequest request) + { + 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); + 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) + { + log.info("allMetricsGet(): baguette-client-ids={} --- client: {}:{}", clientIds, request.getRemoteAddr(), request.getRemotePort()); + Map message1 = createServerMetricsResult(null, -1L); + 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) + { + 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); + 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) { + log.trace("createServerMetricsResult: BEGIN: sid={}, seq={}", sid, sequence); + Map metrics = new LinkedHashMap<>(emsInfoService.getServerMetricValues()); + metrics.put("WEBSSH-BASE-URL", System.getenv("WEBSSH_BASE_URL")); + System.getenv().forEach((property,value) -> { + if (property.startsWith("WEB_ADMIN_")) { + metrics.put(property.substring("WEB_ADMIN_".length()), value); + } + }); + 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 = coordinator.clientMap(); + for (Map.Entry entry : metrics.entrySet()) { + Map info = clientsInfo.get(entry.getKey()); + Object o = entry.getValue(); + if (o instanceof Map) { + ((Map)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; + } +} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/info/SystemInfoProvider.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/SystemInfoProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..9d7b25e412920d7e62efcf4fed9e7abf9723cb78 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/SystemInfoProvider.java @@ -0,0 +1,64 @@ +/* + * 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/ + */ + +package eu.melodic.event.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/event-management/control-service/src/main/java/eu/melodic/event/control/info/SystemResourceMonitor.java b/event-management/control-service/src/main/java/eu/melodic/event/control/info/SystemResourceMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..dc1c24b0ac37c7ccb046b8286158f80edab4937e --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/info/SystemResourceMonitor.java @@ -0,0 +1,129 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.info; + +import eu.melodic.event.brokercep.BrokerCepService; +import eu.melodic.event.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 javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.Map; +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 @Min(1000) + private long period = Math.max(1000L,Long.parseLong( + System.getenv().getOrDefault("EMS_SYSMON_PERIOD", "30000"))); + @Getter @Setter @NotBlank + private String commandStr = System.getenv("EMS_SYSMON_COMMAND"); + @Getter @Setter @NotBlank + private String systemResourceMetricsTopic = System.getenv("EMS_SYSMON_TOPIC"); + + private final BrokerCepService brokerCepService; + private final TaskScheduler scheduler; + private ScheduledFuture future; + @Getter + private Map latestMeasurements; + + @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, 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; + 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); + + processOutput(result.toString()); + + } catch (IOException e) { + log.warn("SystemResourceMonitor: EXCEPTION: ", e); + } + } + + @SneakyThrows + private void processOutput(String result) { + log.debug("SystemResourceMonitor: processOutput: BEGIN:\n{}", result); + 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); + + if (StringUtils.isBlank(systemResourceMetricsTopic)) { + log.debug("SystemResourceMonitor: processOutput: END: No metrics topic has been not set. Will not publish metrics event"); + return; + } + + 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); + } +} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/properties/ControlServiceProperties.java b/event-management/control-service/src/main/java/eu/melodic/event/control/properties/ControlServiceProperties.java index 32fc35571c7e6c141f85d05c49e926264a380db0..52b96acc5dd817cc1cc6d1e937225b8561a8e132 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/properties/ControlServiceProperties.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/properties/ControlServiceProperties.java @@ -19,6 +19,7 @@ import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; @Data @Validated @@ -46,7 +47,7 @@ public class ControlServiceProperties { CLOUDIATOR, PROACTIVE } - @Value("${dontPrintBuildInfo:true}") + @Value("${control.printBuildInfo:false}") private boolean printBuildInfo; @Value("${IP_SETTING:}") private IpSetting ipSetting; @@ -73,6 +74,8 @@ public class ControlServiceProperties { private boolean skipBrokerCep; @Value("${control.skip-baguette:false}") private boolean skipBaguette; + @Value("${control.skip-collectors:false}") + private boolean skipCollectors; @Value("${control.skip-metasolver:false}") private boolean skipMetasolver; @Value("${control.skip-esb-notification:false}") @@ -95,9 +98,18 @@ public class ControlServiceProperties { @Value("${static.resource.context:/**}") private String staticResourceContext; - @Value("${password-encoder-class}") + @Value("${password-encoder-class:}") private String passwordEncoderClass; + @Value("${info.metrics.update.interval:1000}") @Min(1) + private long metricsUpdateInterval; + @Value("${info.client.metrics.update.interval:10000}") @Min(1) + private long metricsClientUpdateInterval; + @Value("${info.metrics.stream.update.interval:10}") @Min(1) + private int metricsStreamUpdateInterval; // in seconds + @Value("${info.metrics.stream.event.name:ems-metrics-event}") @NotBlank + private String metricsStreamEventName; + // control.ssl.** settings private KeystoreAndCertificateProperties ssl; } diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/util/TaskSchedulerConfig.java b/event-management/control-service/src/main/java/eu/melodic/event/control/util/TaskSchedulerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..66623682584c737a2af428da73e4e030c9b48218 --- /dev/null +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/util/TaskSchedulerConfig.java @@ -0,0 +1,36 @@ +/* + * 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/ + */ + +package eu.melodic.event.control.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import javax.validation.constraints.Min; + +@Slf4j +@Configuration +@EnableScheduling +public class TaskSchedulerConfig { + @Value("${control.task-scheduler.thread-pool-size:2}") + @Min(1) + private int threadPoolSize; + + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + log.info("TaskSchedulerConfig: TaskScheduler thread pool size: {}", threadPoolSize); + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(threadPoolSize); + return threadPoolTaskScheduler; + } +} diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/util/TopicBeacon.java b/event-management/control-service/src/main/java/eu/melodic/event/control/util/TopicBeacon.java index 45261cc2f925f25de3716ee31e033180c69641db..da7fc25f5cf79252e496c2f4eab7628dd67f46cc 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/util/TopicBeacon.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/util/TopicBeacon.java @@ -15,8 +15,10 @@ import eu.melodic.event.baguette.server.NodeRegistryEntry; import eu.melodic.event.brokercep.BrokerCepService; import eu.melodic.event.brokercep.event.EventMap; import eu.melodic.event.control.ControlServiceCoordinator; +import eu.melodic.event.translate.TranslationContext; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -26,15 +28,13 @@ import org.springframework.stereotype.Service; import javax.jms.JMSException; import java.io.Serializable; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; +@Slf4j @Service @EnableScheduling -@Slf4j public class TopicBeacon implements InitializingBean { // Topic Beacon settings @Value("${beacon.enable:true}") @@ -55,8 +55,8 @@ public class TopicBeacon implements InitializingBean { private Set beaconPredictionTopics; @Value("${beacon.topics.prediction.rate:60000}") private long beaconPredictionRate; - @Value("${beacon.topics.slo-violator:}") - private Set beaconSloViolatorTopics; + @Value("${beacon.topics.slo-violation-detector:}") + private Set beaconSloViolationDetectorTopics; @Autowired private ControlServiceCoordinator coordinator; @@ -66,6 +66,8 @@ public class TopicBeacon implements InitializingBean { private TaskScheduler scheduler; private Gson gson; + private String previousModelId = ""; + private final AtomicLong modelVersion = new AtomicLong(0); @Override public void afterPropertiesSet() throws Exception { @@ -93,6 +95,7 @@ public class TopicBeacon implements InitializingBean { public void transmitInfo() throws JMSException { log.debug("Topic Beacon: Start transmitting info: {}", new Date()); + updateModelVersion(); transmitHeartbeat(); transmitThresholdInfo(); transmitInstanceInfo(); @@ -102,7 +105,7 @@ public class TopicBeacon implements InitializingBean { } public void transmitHeartbeat() throws JMSException { - if (!SetUtils.emptyIfNull(beaconHeartbeatTopics).isEmpty()) return; + if (SetUtils.emptyIfNull(beaconHeartbeatTopics).isEmpty()) return; String message = "TOPIC BEACON HEARTBEAT "+new Date(); log.debug("Topic Beacon: Transmitting Heartbeat info: message={}, topics={}", message, beaconHeartbeatTopics); @@ -110,7 +113,7 @@ public class TopicBeacon implements InitializingBean { } public void transmitThresholdInfo() { - if (!SetUtils.emptyIfNull(beaconThresholdTopics).isEmpty()) return; + if (SetUtils.emptyIfNull(beaconThresholdTopics).isEmpty()) return; if (coordinator.getTranslationContextOfCamelModel(coordinator.getCurrentCamelModelId())==null) return; @@ -129,7 +132,7 @@ public class TopicBeacon implements InitializingBean { } public void transmitInstanceInfo() throws JMSException { - if (!SetUtils.emptyIfNull(beaconInstanceTopics).isEmpty()) return; + if (SetUtils.emptyIfNull(beaconInstanceTopics).isEmpty()) return; if (coordinator.getBaguetteServer().isServerRunning()) { log.debug("Topic Beacon: Transmitting Instance info: topics={}", beaconInstanceTopics); @@ -146,21 +149,34 @@ public class TopicBeacon implements InitializingBean { } public void transmitPredictionInfo() { - if (!SetUtils.emptyIfNull(beaconPredictionTopics).isEmpty()) return; + if (SetUtils.emptyIfNull(beaconPredictionTopics).isEmpty()) return; String modelId = coordinator.getCurrentCamelModelId(); log.trace("Topic Beacon: transmitPredictionInfo: current-camel-model-id: {}", modelId); - Set topLevelMetrics = coordinator.getGlobalGroupingMetrics(modelId); - if (topLevelMetrics==null) - return; - log.debug("Topic Beacon: transmitPredictionInfo: DAG Global-Level Metrics: {}", topLevelMetrics); - List> payload = topLevelMetrics.stream().map(s -> { + //Set topLevelMetrics = coordinator.getGlobalGroupingMetrics(modelId); + //log.debug("Topic Beacon: transmitPredictionInfo: DAG Global-Level Metrics: {}", topLevelMetrics); + Set metricContexts = coordinator.getMetricContextsForPrediction(modelId); + log.debug("Topic Beacon: transmitPredictionInfo: Metric Contexts for prediction: {}", metricContexts); + + // Convert to Translator-to-Forecasting Methods event format + final long currVersion = modelVersion.get(); + List> payload = metricContexts.stream().map(s -> { HashMap map = new HashMap<>(); - map.put("metric", s); + map.put("metric", s.getName()); map.put("level", 3); - map.put("publish_rate", beaconPredictionRate); + map.put("version", currVersion); + map.put("publish_rate", s.getSchedule()!=null + ? s.getSchedule().getIntervalInMillis() : + beaconPredictionRate); return map; }).collect(Collectors.toList()); + log.debug("Topic Beacon: Transmitting Prediction info: Metric Contexts in event format: {}", payload); + + // Skip event sending if payload is empty + if (payload.size()==0) { + log.debug("Topic Beacon: transmitSloViolatorInfo: Payload is empty. Not sending event"); + return; + } String eventPayload = gson.toJson(payload); @@ -174,20 +190,26 @@ public class TopicBeacon implements InitializingBean { } public void transmitSloViolatorInfo() { - if (!SetUtils.emptyIfNull(beaconSloViolatorTopics).isEmpty()) return; + if (SetUtils.emptyIfNull(beaconSloViolationDetectorTopics).isEmpty()) return; String modelId = coordinator.getCurrentCamelModelId(); log.trace("Topic Beacon: transmitSloViolatorInfo: current-camel-model-id: {}", modelId); - List sloMetricDecompositions = coordinator.getSLOMetricDecomposition(modelId); - if (sloMetricDecompositions==null) - return; + //List sloMetricDecompositions = coordinator.getSLOMetricDecomposition(modelId); + Map sloMetricDecompositions = coordinator.getSLOMetricDecomposition(modelId); log.debug("Topic Beacon: transmitSloViolatorInfo: SLO metric decompositions: {}", sloMetricDecompositions); + // Skip event sending if payload is empty + if (sloMetricDecompositions.get("constraints") == null || ((List) sloMetricDecompositions.get("constraints")).size() == 0) { + log.debug("Topic Beacon: transmitSloViolatorInfo: Payload is empty. Not sending event"); + return; + } + + sloMetricDecompositions.put("version", modelVersion.get()); String eventPayload = gson.toJson(sloMetricDecompositions); - log.debug("Topic Beacon: Transmitting SLO Violator info: event={}, topics={}", eventPayload, beaconSloViolatorTopics); + log.debug("Topic Beacon: Transmitting SLO Violator info: event={}, topics={}", eventPayload, beaconSloViolationDetectorTopics); try { - sendMessageToTopics(eventPayload, beaconSloViolatorTopics); + sendMessageToTopics(eventPayload, beaconSloViolationDetectorTopics); } catch (JMSException e) { log.error("Topic Beacon: EXCEPTION while transmitting SLO Violator info: event={}, topics={}, exception: ", eventPayload, beaconPredictionTopics, e); @@ -211,7 +233,20 @@ public class TopicBeacon implements InitializingBean { brokerCepService.getBrokerPassword(), topicName, event); - log.info("Topic Beacon: Event sent to topic: event={}, topic={}", event, topicName); + log.debug("Topic Beacon: Event sent to topic: event={}, topic={}", event, topicName); + } + } + + private synchronized boolean updateModelVersion() { + String modelId = coordinator.getCurrentCamelModelId(); + 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; } } diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/StaticResourceConfiguration.java b/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/StaticResourceConfiguration.java index b5d6455fedb5f1e6c76abe9f0a329d30cc4311ad..58c596232e034e9e4b2a39fe9475acf73e10fe0c 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/StaticResourceConfiguration.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/StaticResourceConfiguration.java @@ -16,26 +16,29 @@ 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.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -@Configuration -@EnableWebMvc +import java.util.Map; + @Slf4j +@Configuration public class StaticResourceConfiguration implements WebMvcConfigurer { @Value("${static.favicon.context:/favicon.ico}") private String faviconContext; @Value("${static.favicon.path:#{null}}") private String faviconPath; - @Value("${static.resource.context:/**}") + @Value("${static.resource.context:/resources/**}") private String staticResourceContext; @Value("${static.resource.path:#{null}}") private String[] staticResourcePath; + @Value("${static.resource.redirect:#{null}}") private String staticResourceRedirect; + @Value("#{${static.resource.redirects:{}}}") + private Map staticResourceRedirects; @Value("${static.logs.context:/logs/**}") private String staticLogsContext; @@ -84,6 +87,7 @@ public class StaticResourceConfiguration implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { + // Remains for backward compatibility (of properties file) if (StringUtils.isNotBlank(staticResourceRedirect)) { log.info("Redirecting / to: {}", staticResourceRedirect); registry @@ -91,6 +95,20 @@ public class StaticResourceConfiguration implements WebMvcConfigurer { .setViewName("redirect:" + staticResourceRedirect); } + log.debug("Configured resource redirects: {}", staticResourceRedirects); + if (staticResourceRedirects!=null) { + staticResourceRedirects.forEach((context, redirect) -> { + if (StringUtils.isNotBlank(context) && StringUtils.isNotBlank(redirect)) { + context = context.trim(); + redirect = redirect.trim(); + log.info("Redirecting {} to: {}", context, redirect); + registry + .addViewController(context) + .setViewName("redirect:" + redirect); + } + }); + } + WebMvcConfigurer.super.addViewControllers(registry); } diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebMvcConfig.java b/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebMvcConfig.java index 69936cda969dd411e85fa73e8ee8a21dd9ad391e..82e80eb462a4c2fc8237d8aea199e3d83c2dd0b3 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebMvcConfig.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebMvcConfig.java @@ -15,20 +15,31 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +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.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Slf4j @Component @ComponentScan(basePackages={"eu.melodic.security.authorization.util.properties"}) -@Slf4j public class WebMvcConfig implements WebMvcConfigurer { private final static String[] DEFAULT_PATHS_PROTECTED = { "/**" }; private final static String[] DEFAULT_PATHS_EXCLUDED = { }; @Autowired private AuthorizationServiceClientProperties authProperties; + @Autowired + private ApplicationContext applicationContext; @Value("${authorization.enabled:true}") private boolean authEnabled; @@ -59,4 +70,18 @@ public class WebMvcConfig implements WebMvcConfigurer { log.debug("WebMvcConfig.addInterceptors(): Registered Authorization interceptor"); } } + + @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); + } } \ No newline at end of file diff --git a/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebSecurityConfig.java b/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebSecurityConfig.java index a6ad2df236ca9948e2d572da91419c88d6a179e8..f39b0e323a295bd9980e0889688b7b602527c8be 100644 --- a/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebSecurityConfig.java +++ b/event-management/control-service/src/main/java/eu/melodic/event/control/webconf/WebSecurityConfig.java @@ -9,13 +9,16 @@ package eu.melodic.event.control.webconf; -import eu.paasage.upperware.security.authapi.JWTAuthorizationFilter; +import eu.melodic.event.util.PasswordUtil; +import eu.paasage.upperware.security.authapi.SecurityConstants; import eu.paasage.upperware.security.authapi.properties.MelodicSecurityProperties; import eu.paasage.upperware.security.authapi.token.JWTService; import eu.paasage.upperware.security.authapi.token.JWTServiceImpl; +import io.jsonwebtoken.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -23,30 +26,39 @@ 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.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -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.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.Filter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.security.InvalidParameterException; +import java.util.Arrays; import java.util.Collections; @Slf4j @Order(1) @Configuration @EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) @EnableConfigurationProperties(MelodicSecurityProperties.class) @RequiredArgsConstructor public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + 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"; + private final MelodicSecurityProperties melodicSecurityProperties; @Value("${melodic.security.enabled:true}") @@ -55,8 +67,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // JWT Token authentication fields @Value("${web.jwt-token-authentication.enabled:true}") private boolean jwtTokenAuthEnabled; + @Value("${web.jwt-print-sample-token:false}") + private boolean printSampleJwt; // API-Key authentication fields + @Value("${web.api-key-authentication.enabled:true}") + private boolean apiKeyAuthEnabled; @Value("${web.api-key.header:EMS-API-KEY}") private String apiKeyRequestHeader; @Value("${web.api-key.parameter:ems-api-key}") @@ -64,68 +80,225 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${web.api-key.value:#{null}}") private String apiKeyValue; + @Autowired + private PasswordUtil passwordUtil; + + // User form authentication fields + @Value("${web.form-authentication.enabled:true}") + private boolean userFormAuthEnabled; + @Value("${web.form-authentication.username:admin}") + private String username; + @Value("${web.form-authentication.password:}") + private String password; + + @Value("${web.permitted.urls:/login*,/logout*,/admin/login.html,/admin/favicon.ico,/admin/assets/**,/resources/*}") + private String[] permittedUrls; + @Value("${web.login.page:/admin/login.html}") + private String loginPage; + @Value("${web.login.url:/login}") + private String loginUrl; + @Value("${web.login.success.url:/}") + private String loginSuccessUrl; + @Value("${web.login.failure.url:/admin/login.html?error=Invalid+username+or+password}") + private String loginFailureUrl; + @Value("${web.logout.url:/logout}") + private String logoutUrl; + @Value("${web.logout.success.url:/admin/login.html?message=Signed+out}") + private String logoutSuccessUrl; + + private final static String divider = "--------------------------------------------------------------------------------"; @EventListener(ApplicationReadyEvent.class) public void applicationReady() { - log.warn("====> afterPropertiesSet: Sample JWT Token: \nBearer {}", jwtService(melodicSecurityProperties).create("USER")); + if (userFormAuthEnabled && (StringUtils.isBlank(username) || password.isEmpty())) + throw new InvalidParameterException("User form authentication is enabled but username or password are blank"); + if (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]; + + log.info("afterPropertiesSet: Admin Username: {}", username); + log.info("afterPropertiesSet: Admin Password: {}", passwordUtil.encodePassword(password)); + log.info("afterPropertiesSet: API Key: {}", passwordUtil.encodePassword(apiKeyValue)); + if (printSampleJwt) + log.info("afterPropertiesSet:\n{}\nSample JWT Token: \nBearer {}\n{}", + divider, jwtService(melodicSecurityProperties).create("USER"), divider); + + log.debug("afterPropertiesSet: securityEnabled: {}", securityEnabled); + log.debug("afterPropertiesSet: jwtTokenAuthEnabled: {}", jwtTokenAuthEnabled); + log.debug("afterPropertiesSet: apiKeyRequestHeader: {}", apiKeyRequestHeader); + log.debug("afterPropertiesSet: apiKeyRequestParam: {}", apiKeyRequestParam); + log.debug("afterPropertiesSet: permittedUrls: {}", Arrays.asList(permittedUrls)); + 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); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + if (this.userFormAuthEnabled && StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) { + auth.inMemoryAuthentication() + .withUser(username).password(passwordEncoder().encode(password)).authorities(ROLE_USER_FORM); + log.info("WebSecurityConfig: User Form Admin credentials have been set: username={}", username); + } + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring() - // Spring Security should completely ignore URLs starting with /resources/ - .antMatchers("/favicon.ico") - .antMatchers("/health"); + // Spring Security should completely ignore the following URLs + .antMatchers("/favicon.ico", "/health"); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { - // Disable CSRF and sessions (it's a REST API) - httpSecurity - .csrf().disable() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - - // No security at all - boolean apiKeyAuthEnabled = StringUtils.isNotBlank(apiKeyValue); - log.debug("WebSecurityConfig: security-enabled={}, jwt-auth-enabled={}, api-key-auth-enabled={}", - securityEnabled, jwtTokenAuthEnabled, apiKeyAuthEnabled); - if (!securityEnabled || !jwtTokenAuthEnabled && !apiKeyAuthEnabled) { + + // 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={}", + securityEnabled, userFormAuthEnabled, jwtTokenAuthEnabled, apiKeyAuthEnabled); + if (!securityEnabled || !userFormAuthEnabled && !jwtTokenAuthEnabled && !apiKeyAuthEnabled) { log.warn("WebSecurityConfig: Authentication is disabled"); // Authorize all requests httpSecurity + .csrf().disable() .authorizeRequests() - .antMatchers("/**").permitAll(); + .anyRequest().permitAll(); return; } + // Add and Configure User Form authentication + if (userFormAuthEnabled) { + log.info("WebSecurityConfig: User form Authentication is enabled"); + httpSecurity + .csrf().disable() + .authorizeRequests() + //.antMatchers("//broker/credentials").hasAnyAuthority(ROLE_JWT_TOKEN, ROLE_API_KEY) + //.antMatchers("/baguette/ref/**").hasAnyAuthority(ROLE_JWT_TOKEN, ROLE_API_KEY) + .antMatchers(permittedUrls).permitAll() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage(loginPage).permitAll() + .loginProcessingUrl(loginUrl).permitAll() + //.usernameParameter("username") + //.passwordParameter("password") + .defaultSuccessUrl(loginSuccessUrl, false) + .failureUrl(loginFailureUrl).permitAll() + .and() + .logout() + .logoutUrl(logoutUrl).permitAll() + .logoutSuccessUrl(logoutSuccessUrl).permitAll() + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID"); + log.debug("WebSecurityConfig: User form Authentication has been configured"); + } else { + httpSecurity + .csrf().disable() + .authorizeRequests() + .anyRequest().authenticated(); + } + // Add configured authentication filters if (apiKeyAuthEnabled) { - log.debug("WebSecurityConfig: Adding API-Key filter"); + log.info("WebSecurityConfig: API-Key Authentication is enabled"); httpSecurity - .addFilterBefore(apiKeyAuthenticationFilter(), BasicAuthenticationFilter.class); + .addFilterAfter(apiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) + .authorizeRequests() + .anyRequest().authenticated(); + log.debug("WebSecurityConfig: API-Key Authentication filter added"); } if (jwtTokenAuthEnabled) { - log.debug("WebSecurityConfig: Adding JWT-Token filter"); + log.info("WebSecurityConfig: JWT-Token Authentication is enabled"); httpSecurity - .addFilterAfter(new JWTAuthorizationFilter(authenticationManager(), jwtService(melodicSecurityProperties)), - BasicAuthenticationFilter.class); + .addFilterAfter(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class) + .authorizeRequests() + .anyRequest().authenticated(); + log.debug("WebSecurityConfig: JWT-Token Authentication filter added"); } - - // Apply authentication to all URLs - httpSecurity - .authorizeRequests() - .antMatchers("/**").authenticated(); } - @Bean - protected AuthenticationManager authenticationManager() { - return authentication -> authentication; + 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 have been set. It will not be possible to authenticate using API Key"); + } } public JWTService jwtService(MelodicSecurityProperties melodicSecurityProperties) { return new JWTServiceImpl(melodicSecurityProperties); } + public Filter jwtAuthorizationFilter() { + return (servletRequest, servletResponse, filterChain) -> { + if (servletRequest instanceof HttpServletRequest) { + HttpServletRequest req = (HttpServletRequest) servletRequest; + + String header = req.getHeader(SecurityConstants.HEADER_STRING); + log.debug("jwtAuthorizationFilter: Authorization Header: {}", header); + if (header!=null && header.startsWith(SecurityConstants.TOKEN_PREFIX)) { + try { + log.debug("jwtAuthorizationFilter: Parsing Authorization header..."); + Claims claims = jwtService(melodicSecurityProperties).parse(header); + 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 (SecurityConstants.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 (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException 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 down the chain + filterChain.doFilter(servletRequest, servletResponse); + }; + } + public Filter apiKeyAuthenticationFilter() { return (servletRequest, servletResponse, filterChain) -> { log.trace("apiKeyAuthenticationFilter: BEGIN: request={}", servletRequest); @@ -134,17 +307,33 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { HttpServletRequest request = (HttpServletRequest) servletRequest; log.trace("apiKeyAuthenticationFilter: http-request={}", request); String apiKey = request.getHeader(apiKeyRequestHeader); - log.debug("apiKeyAuthenticationFilter: Request Header API Key: {}={}", apiKeyRequestHeader, apiKey); + 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, apiKey); + log.debug("apiKeyAuthenticationFilter: Request Parameter API Key: {}={}", apiKeyRequestParam, passwordUtil.encodePassword(apiKey)); } - if (StringUtils.isBlank(apiKey) || StringUtils.isNotBlank(apiKey) && ! apiKey.equals(apiKeyValue)) { - log.debug("apiKeyAuthenticationFilter: API Key: No Match"); - ((HttpServletResponse) servletResponse).setStatus(401); - return; + 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 header or parameters"); } - log.debug("apiKeyAuthenticationFilter: API Key: Matched"); } 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()); @@ -153,18 +342,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { log.warn("apiKeyAuthenticationFilter: No API-Key specified. Access is granted"); } - try { - // construct one of Spring's auth tokens - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(apiKeyRequestHeader, apiKeyValue, Collections.singletonList( - new SimpleGrantedAuthority("USER-ROLE"))); - // store completed authentication in security context - SecurityContextHolder.getContext().setAuthentication(authentication); - // continue down the chain. - filterChain.doFilter(servletRequest, servletResponse); - } catch (Exception e) { - log.error("apiKeyAuthenticationFilter: EXCEPTION: ", e); - } + // continue down the chain + filterChain.doFilter(servletRequest, servletResponse); }; } } \ No newline at end of file diff --git a/event-management/control-service/src/main/resources/public/index.html b/event-management/control-service/src/main/resources/public/index.html index 6cf1f219fa22c66f7841e5d6f3d628a04bfd244a..6b17b9703d3c51b932312ac573aa83b1f5247f01 100644 --- a/event-management/control-service/src/main/resources/public/index.html +++ b/event-management/control-service/src/main/resources/public/index.html @@ -8,7 +8,7 @@ --> - + //--> + -

EMS - Event Generation and Publish

- - - - - -

Settings

- - - - -
Base URL:
- - +

EMS - Event Generation and Publish

-

Send Commands to Clients - [List] - [Map]

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ClientCommandActionsResult
- - + + + + + + + - - - -

Settings

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

Event Publishing

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SesnorClientValueActionsResult
- - - -

Event Generation

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SesnorClientIntervalLower ValueUpper ValueActionsResult
- -
- -
- -
- -
- -
- - + +

Send Commands to Clients

+ + + +

[List] + [Map]

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

Event Publishing

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

Event Generation

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

Live Metrics:

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

Statistics:

+ + + + + + -

Downloads:

- - - + +

Downloads:

+ + + + + + + -

Statistics:

- - - - - \ No newline at end of file diff --git a/event-management/extra/src/main/java/eu/melodic/event/extra/cloudiator/CloudiatorInstallationHelper.java b/event-management/extra/src/main/java/eu/melodic/event/extra/cloudiator/CloudiatorInstallationHelper.java index c7ab789286a78d56bee0c704947837d2e85b3b83..76dee2865b0b94184516c63d4a378cb627ec64e6 100644 --- a/event-management/extra/src/main/java/eu/melodic/event/extra/cloudiator/CloudiatorInstallationHelper.java +++ b/event-management/extra/src/main/java/eu/melodic/event/extra/cloudiator/CloudiatorInstallationHelper.java @@ -13,8 +13,9 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import eu.melodic.event.baguette.client.install.ClientInstallationTask; import eu.melodic.event.baguette.client.install.helper.AbstractInstallationHelper; -import eu.melodic.event.baguette.client.install.instruction.InstallationInstructions; +import eu.melodic.event.baguette.client.install.instruction.InstructionsSet; import eu.melodic.event.baguette.server.BaguetteServer; +import eu.melodic.event.baguette.server.NodeRegistryEntry; import eu.melodic.event.util.CredentialsMap; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -49,21 +50,24 @@ public class CloudiatorInstallationHelper extends AbstractInstallationHelper { } @Override - public ClientInstallationTask createClientInstallationTask(Map nodeMap, Map contextMap, BaguetteServer baguette) throws Exception { + public ClientInstallationTask createClientInstallationTask(NodeRegistryEntry entry) throws Exception { return null; } @Override - public List prepareInstallationInstructionsForWin(Map nodeMap, Map contextMap, BaguetteServer baguette) { + public List prepareInstallationInstructionsForWin(NodeRegistryEntry entry) { log.warn("CloudiatorInstallationHelper.prepareInstallationInstructionsForWin(): NOT YET IMPLEMENTED"); throw new IllegalArgumentException("CloudiatorInstallationHelper.prepareInstallationInstructionsForWin(): NOT YET IMPLEMENTED"); } @Override - public List prepareInstallationInstructionsForLinux(Map nodeMap, Map contextMap, BaguetteServer baguette) throws IOException { - String baseUrl = contextMap.get("BASE_URL"); - String clientId = contextMap.get("CLIENT_ID"); - String ipSetting = contextMap.get("IP_SETTING"); + public List prepareInstallationInstructionsForLinux(NodeRegistryEntry entry) throws IOException { + Map nodeMap = entry.getPreregistration(); + BaguetteServer baguette = entry.getBaguetteServer(); + + String baseUrl = nodeMap.get("BASE_URL"); + String clientId = nodeMap.get("CLIENT_ID"); + String ipSetting = nodeMap.get("IP_SETTING"); log.debug("CloudiatorInstallationHelper.prepareInstallationInstructionsForLinux(): Invoked: base-url={}", baseUrl); // Get parameters @@ -100,8 +104,8 @@ public class CloudiatorInstallationHelper extends AbstractInstallationHelper { valueMap.put("IP_SETTING", ipSetting); // Set the target operating system - InstallationInstructions installationInstructions = new InstallationInstructions(); - installationInstructions.setOs("LINUX"); + InstructionsSet instructionsSet = new InstructionsSet(); + instructionsSet.setOs("LINUX"); // Check whether EMS Client is already installed /*.appendLog("Checking if Baguette Client is already installed") @@ -109,16 +113,16 @@ public class CloudiatorInstallationHelper extends AbstractInstallationHelper { .appendExec("Baguette Client is NOT installed")*/ // Create Baguette Client installation directories - installationInstructions.appendLog("Create Baguette Client installation directories"); + instructionsSet.appendLog("Create Baguette Client installation directories"); String dirList = String.join(" ", properties.getMkdirs()); if (StringUtils.isNotEmpty(dirList)) - installationInstructions.appendExec("sudo mkdir -p " + dirList); + instructionsSet.appendExec("sudo mkdir -p " + dirList); // Create files using touch - installationInstructions.appendLog("Touch files"); + instructionsSet.appendLog("Touch files"); String touchList = String.join(" ", properties.getTouchFiles()); if (StringUtils.isNotEmpty(touchList)) - installationInstructions.appendExec("sudo touch " + touchList); + instructionsSet.appendExec("sudo touch " + touchList); // Clear EMS server certificate (PEM) file, if not secure if (!isServerSecure) { @@ -135,13 +139,13 @@ public class CloudiatorInstallationHelper extends AbstractInstallationHelper { .sorted() .collect(Collectors.toList()); for (Path p : paths) { - _appendCopyInstructions(installationInstructions, p, startDir, copyToClientDir, clientTmpDir, valueMap); + _appendCopyInstructions(instructionsSet, p, startDir, copyToClientDir, clientTmpDir, valueMap); } } } // Download Baguette Client installation script - installationInstructions + instructionsSet .appendLog("Download Baguette Client installation script") //.appendExec("sudo wget --no-check-certificate " + installScriptUrl + " -O " + installScriptPath) .appendExec( @@ -171,16 +175,16 @@ public class CloudiatorInstallationHelper extends AbstractInstallationHelper { .appendExec("sudo touch " + checkInstallationFile)*/ ; - // Pretty print installationInstructions JSON + // Pretty print instructionsSet JSON if (log.isDebugEnabled()) { Gson gson = new GsonBuilder().setPrettyPrinting().create(); StringWriter sw = new StringWriter(); try (PrintWriter writer = new PrintWriter(sw)) { - gson.toJson(installationInstructions, writer); + gson.toJson(instructionsSet, writer); } - log.debug("prepareInstallationInstructionsForLinux(): installationInstructions:\n{}", sw.toString()); + log.debug("prepareInstallationInstructionsForLinux(): instructionsSet:\n{}", sw.toString()); } - return Collections.singletonList(installationInstructions); + return Collections.singletonList(instructionsSet); } } diff --git a/event-management/pom.xml b/event-management/pom.xml index 44e45867a696a58e3492267bfd37d82d67f55568..9a594fc0bd0a57c84e2cfa73cb40ff2f7bc9e7d5 100644 --- a/event-management/pom.xml +++ b/event-management/pom.xml @@ -47,6 +47,7 @@ translator broker-client broker-cep + common baguette-client baguette-client-install baguette-server @@ -80,5 +81,22 @@ extra + + build-web-admin + + true + + + web-admin + + + + eu.melodic.event + web-admin + ${project.version} + pom + + + diff --git a/event-management/translator/pom.xml b/event-management/translator/pom.xml index 9c82bb0931fa3f10ce52abb47d18a57acee4448e..fc9d32203f9d38927cb92ac76cc39677be099c8f 100644 --- a/event-management/translator/pom.xml +++ b/event-management/translator/pom.xml @@ -52,11 +52,11 @@ client repackaged - + org.ow2.paasage upperware-metamodel diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/CamelToEplTranslator.java b/event-management/translator/src/main/java/eu/melodic/event/translate/CamelToEplTranslator.java index 3ea42c8fa6644805dcc90fc1911514702da7a08f..b2b000f2c3522bef190561950b096b005fd738a1 100644 --- a/event-management/translator/src/main/java/eu/melodic/event/translate/CamelToEplTranslator.java +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/CamelToEplTranslator.java @@ -204,6 +204,8 @@ public class CamelToEplTranslator implements Translator { log.info("*********************************************************"); log.info("MVV_CP map:\n{}", _TC.MVV_CP); log.info("*********************************************************"); + log.info("CMVAR set:\n{}", _TC.CMVAR); + log.info("*********************************************************"); log.info("Function Definitions set:\n{}", getFunctionNames(_TC.FUNC)); log.info("*********************************************************"); log.info("Metric Constraints:\n{}", _TC.getMetricConstraints()); diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/TranslationContext.java b/event-management/translator/src/main/java/eu/melodic/event/translate/TranslationContext.java index 2286b5d85dd8cf9ff5eb2c59aa1f784f3c3d68a3..7befbc5e04b31ba30a1be048299ec7165f9c27a3 100644 --- a/event-management/translator/src/main/java/eu/melodic/event/translate/TranslationContext.java +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/TranslationContext.java @@ -10,6 +10,7 @@ package eu.melodic.event.translate; import camel.constraint.ComparisonOperatorType; +import camel.constraint.Constraint; import camel.constraint.UnaryConstraint; import camel.core.Action; import camel.core.NamedElement; @@ -22,11 +23,13 @@ import eu.melodic.event.translate.analyze.DAG; import eu.melodic.event.translate.analyze.DAGNode; import eu.melodic.event.util.FunctionDefinition; import eu.melodic.models.interfaces.ems.Monitor; +import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @@ -58,7 +61,7 @@ public class TranslationContext { // Grouping-to-Topics map public final Map> G2T; // Metric-to-Metric Context map - public final Map> M2MC; + public final Map> M2MC; // Composite Metric Variables set public final Set CMVAR; public final Set CMVAR_1; @@ -81,6 +84,8 @@ public class TranslationContext { protected Set metricConstraints; // Logical Constraints protected Set logicalConstraints; + // If-Then-Else Constraints + protected Set ifThenConstraints; // ==================================================================================================================================================== @@ -124,6 +129,8 @@ public class TranslationContext { this.metricConstraints = new HashSet<>(); // Logical Constraints this.logicalConstraints = new HashSet<>(); + // If-Then-Else Constraints + this.ifThenConstraints = new HashSet<>(); } // ==================================================================================================================================================== @@ -149,8 +156,8 @@ public class TranslationContext { return newGroupingsMap; } - public MetricContext getMetricContextForMetric(Metric m) { - Set set = M2MC.get(m); + public camel.metric.MetricContext getMetricContextForMetric(Metric m) { + Set set = M2MC.get(m); return set == null ? null : set.iterator().next(); } @@ -167,6 +174,14 @@ public class TranslationContext { return new HashSet<>(logicalConstraints); } + public Set getIfThenConstraints() { + return new HashSet<>(ifThenConstraints); + } + + public Set getMVVs() { return new HashSet<>(MVV); } + + public Set getCompositeMetricVariables() { return new HashSet<>(CMVAR); } + // ==================================================================================================================================================== // Map- and Set-related helper methods @@ -235,11 +250,11 @@ public class TranslationContext { rules.forEach(rule -> addGroupingRulePair(grouping, topic, rule)); } - public void addMetricMetricContextPair(Metric m, MetricContext mc) { + public void addMetricMetricContextPair(Metric m, camel.metric.MetricContext mc) { _addPair(M2MC, m, mc); } - public void addMetricMetricContextPairs(Metric m, List mcs) { + public void addMetricMetricContextPairs(Metric m, List mcs) { _addPair(M2MC, m, mcs); } @@ -282,7 +297,7 @@ public class TranslationContext { String metricName = null; if (uc instanceof camel.constraint.MetricConstraint) { camel.constraint.MetricConstraint mc = (camel.constraint.MetricConstraint) uc; - MetricContext context = mc.getMetricContext(); + camel.metric.MetricContext context = mc.getMetricContext(); if (context!=null) metricName = context.getName(); if (StringUtils.isBlank(metricName)) throw new IllegalArgumentException("Metric Constraint '"+uc.getName()+"' has no valid metric context"); @@ -316,6 +331,21 @@ public class TranslationContext { logicalConstraints.add(new LogicalConstraint(logicalConstraint.getName(), opName, childConstraintNames, nodeList)); } + public void addIfThenConstraint(camel.constraint.IfThenConstraint ifThenConstraint) { + String name = ifThenConstraint.getName(); + + // Get child constraints + Constraint ifConstraint = ifThenConstraint.getIf(); + Constraint thenConstraint = ifThenConstraint.getThen(); + Constraint elseConstraint = ifThenConstraint.getElse(); + String ifConstraintName = ifConstraint.getName(); + String thenConstraintName = thenConstraint.getName(); + String elseConstraintName = elseConstraint != null ? elseConstraint.getName() : null; + + // Add logical constraint information + ifThenConstraints.add(new IfThenConstraint(name, ifConstraintName, thenConstraintName, elseConstraintName)); + } + // ==================================================================================================================================================== // Topic-Connections-per-Grouping-related helper methods // Auto-fill of Topic connections between Groupings.... (use provide/require methods below) @@ -482,4 +512,44 @@ public class TranslationContext { private final List constraints; private final List constraintNodes; } + + @lombok.Data + public static class IfThenConstraint { + private final String name; + private final String ifConstraintName; + private final String thenConstraintName; + private final String elseConstraintName; + } + + @lombok.Data + @RequiredArgsConstructor + public static class MetricContext { + private final String name; + private final Schedule schedule; + + public MetricContext(camel.metric.MetricContext mc) { + name = mc.getName(); + schedule = (mc.getSchedule()!=null) ? new TranslationContext.Schedule(mc.getSchedule()) : null; + } + } + + @lombok.Data + @RequiredArgsConstructor + public static class Schedule { + private final String name; + private final String unit; + private final long interval; + private final int repetitions; + private final Date start; + private final Date end; + + public Schedule(camel.metric.Schedule s) { + this(s.getName(), s.getTimeUnit().getName(), s.getInterval(), s.getRepetitions(), s.getStart(), s.getEnd()); + } + + public long getIntervalInMillis() { + if (unit==null) return interval; + return TimeUnit.MILLISECONDS.convert(interval, TimeUnit.valueOf(unit.toUpperCase())); + } + } } diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAG.java b/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAG.java index 81b86ac9a106cd4fea80bf98c99f59c3cbd8421a..75fa772d2240306358c5bece283f81269504b0e5 100644 --- a/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAG.java +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAG.java @@ -55,17 +55,22 @@ public class DAG { } public Set getTopLevelNodes() { - log.info("DAG.getTopLevelNodes()"); - Set children = _graph.outgoingEdgesOf(_root).stream().map(edge -> edge.getTarget()).collect(java.util.stream.Collectors.toSet()); - log.info("DAG.getTopLevelNodes(): top-level-nodes={}", children); + 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); - Iterator it = parents.iterator(); - while (it.hasNext()) { - if (it.next() == _root) return true; + for (DAGNode parent : parents) { + if (parent == _root) return true; } return false; } @@ -88,9 +93,9 @@ public class DAG { public Set getNodeChildren(DAGNode node) { try { - //log.info("DAG.getNodeChildren(): node={}", node); + //log.debug("DAG.getNodeChildren(): node={}", node); Set children = _graph.outgoingEdgesOf(node).stream().map(edge -> edge.getTarget()).collect(java.util.stream.Collectors.toSet()); - //log.info("DAG.getNodeChildren(): parent={}, children={}", node, children); + //log.debug("DAG.getNodeChildren(): parent={}, children={}", node, children); return children; } catch (IllegalArgumentException iae) { log.warn("DAG.getNodeChildren(): Node not in DAG: node={}", node); @@ -123,8 +128,8 @@ public class DAG { node = new DAGNode(elem, fullName); newNode = _graph.addVertex(node); - if (newNode) log.info("DAG.addTopLevelNode(): Element added in DAG: {}", node.getName()); - else log.info("DAG.addTopLevelNode(): Element already in DAG and replaced: {}", node.getName()); + 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) { @@ -135,19 +140,19 @@ public class DAG { } else { node = _nameToNodesMapping.get(fullName); newNode = _graph.addVertex(node); - if (newNode) log.info("DAG.addTopLevelNode()-2: Element added in DAG: {}", node.getName()); - else log.info("DAG.addTopLevelNode()-2: Element already in DAG and replaced: {}", node.getName()); + 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.info("DAG.addTopLevelNode(): Element already in DAG: {}", node.getName()); + log.debug("DAG.addTopLevelNode(): Element already in DAG: {}", node.getName()); } DAGEdge edge = new DAGEdge(); boolean newEdge = _graph.addEdge(_root, node, edge); - if (newNode) log.info("DAG.addTopLevelNode(): Element set as Top-Level in DAG: {}", node.getName()); - else log.info("DAG.addTopLevelNode(): Element is already set as Top-Level in DAG: {}", node.getName()); + 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; } @@ -167,8 +172,8 @@ public class DAG { node = new DAGNode(elem, fullName); newNode = _graph.addVertex(node); - if (newNode) log.info("DAG.addNode(): Element added in DAG: {}", node.getName()); - else log.info("DAG.addNode(): Element already in DAG and replaced: {}", node.getName()); + 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) { @@ -179,20 +184,20 @@ public class DAG { } else { node = _nameToNodesMapping.get(fullName); newNode = _graph.addVertex(node); - if (newNode) log.info("DAG.addNode()-2: Element added in DAG: {}", node.getName()); - else log.info("DAG.addNode()-2: Element already in DAG and replaced: {}", node.getName()); + 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.info("DAG.addNode(): Element already in DAG: {}", node.getName()); + 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.info("DAG.addNode(): Edge added in DAG: {} --> {} ", parent.getName(), node.getName()); - else log.info("DAG.addNode(): Edge is already in DAG: {} --> {}", parent.getName(), node.getName()); + 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; } @@ -214,13 +219,9 @@ public class DAG { throw new RuntimeException("Element being removed has children: " + node.getName()); // remove node from DAG - if (node != null) { - _graph.removeVertex(node); // This also removes edges touching this node - _namedElementToNodesMapping.remove(elem); - log.error("DAG.removeNode(): Element removed from DAG: {}", node.getName()); - } else { - log.error("DAG.removeNode(): Element not found in DAG: {}", node.getName()); - } + _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; } @@ -244,8 +245,8 @@ public class DAG { if (nodeFrom != null && nodeTo != null) { DAGEdge edge = new DAGEdge(); boolean newEdge = _graph.addEdge(nodeFrom, nodeTo, edge); - if (newEdge) log.info("DAG.addEdge(): Edge added in DAG: {} --> {} ", elemFrom.getName(), elemTo.getName()); - else log.info("DAG.addEdge(): Edge is already in DAG: {} --> {}", elemFrom.getName(), elemTo.getName()); + 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", @@ -257,7 +258,7 @@ public class DAG { 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.info("DAG.addEdge(): Adding edge in DAG: {} --> {} ", elemFrom, elemTo); + log.debug("DAG.addEdge(): Adding edge in DAG: {} --> {} ", elemFrom, elemTo); Iterator it = _graph.iterator(); DAGNode nodeFrom = null; @@ -270,8 +271,8 @@ public class DAG { if (nodeFrom != null && nodeTo != null) { DAGEdge edge = new DAGEdge(); boolean newEdge = _graph.addEdge(nodeFrom, nodeTo, edge); - if (newEdge) log.info("DAG.addEdge(): Edge added in DAG: {} --> {} ", elemFrom, elemTo); - else log.info("DAG.addEdge(): Edge is already in DAG: {} --> {}", elemFrom, elemTo); + 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", @@ -295,8 +296,8 @@ public class DAG { if (nodeFrom != null && nodeTo != null) { DAGEdge deletedEdge = _graph.removeEdge(nodeFrom, nodeTo); if (deletedEdge != null) - log.info("DAG.removeEdge(): Edge removed from DAG: {} --> {} ", elemFrom.getName(), elemTo.getName()); - else log.info("DAG.removeEdge(): Edge not found in DAG: {} --> {}", elemFrom.getName(), elemTo.getName()); + 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", @@ -311,9 +312,9 @@ public class DAG { DAGEdge deletedEdge = _graph.removeEdge(nodeFrom, nodeTo); if (deletedEdge != null) - log.info("DAG.removeEdge(): Edge removed from DAG: {} --> {} ", nodeFrom.getElementName(), nodeTo.getElementName()); + log.debug("DAG.removeEdge(): Edge removed from DAG: {} --> {} ", nodeFrom.getElementName(), nodeTo.getElementName()); else - log.info("DAG.removeEdge(): Edge not found in DAG: {} --> {}", nodeFrom.getElementName(), nodeTo.getElementName()); + log.warn("DAG.removeEdge(): Edge not found in DAG: {} --> {}", nodeFrom.getElementName(), nodeTo.getElementName()); return deletedEdge; } @@ -321,9 +322,9 @@ public class DAG { // Traverse graph methods public void traverseDAG(java.util.function.Consumer action) { - log.info("DAG.traverseDAG(): Traversing graph: Begin"); + log.debug("DAG.traverseDAG(): Traversing graph: Begin"); _graph.iterator().forEachRemaining(action); - log.info("DAG.traverseDAG(): Traversing graph: End"); + log.debug("DAG.traverseDAG(): Traversing graph: End"); } // ==================================================================================================================================================== @@ -408,6 +409,6 @@ public class DAG { } public String toString() { - return _graph.toString(); + return _graph!=null ? _graph.toString() : null; } } diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAGNode.java b/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAGNode.java index be3b4a03a0ccf164cc072e41bbdfabb0c622bd5f..b4a527abdbdf7a4c80e48a7f6c301ee3c0e068f6 100644 --- a/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAGNode.java +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/DAGNode.java @@ -10,6 +10,8 @@ package eu.melodic.event.translate.analyze; import camel.core.NamedElement; +import camel.metric.MetricContext; +import eu.melodic.event.translate.TranslationContext; import java.util.concurrent.atomic.AtomicLong; @@ -20,6 +22,7 @@ public class DAGNode { private final long _id = counter.getAndIncrement(); private final String _name; private final String _toString; + private final TranslationContext.MetricContext metricContext; private Grouping grouping; DAGNode() { @@ -27,6 +30,7 @@ public class DAGNode { elementName = null; _name = null; _toString = "NODE "; + metricContext = null; } public DAGNode(NamedElement elem, String fullName) { @@ -36,6 +40,12 @@ public class DAGNode { elementName = element.getName(); _name = fullName; _toString = String.format("NODE %s", _name); + + if (elem instanceof MetricContext) { + MetricContext mc = (MetricContext) elem; + metricContext = new TranslationContext.MetricContext(mc); + } else + metricContext = null; } public long getId() { @@ -64,6 +74,10 @@ public class DAGNode { return elementName; } + public TranslationContext.MetricContext getMetricContext() { + return metricContext; + } + public int hashCode() { return toString().hashCode(); } diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/ModelAnalyzer.java b/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/ModelAnalyzer.java index e30bdfbd0909b41bd6822cf740495499ef337c78..ca6fbddf744985bd5359ca791310d603454cfcae 100644 --- a/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/ModelAnalyzer.java +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/analyze/ModelAnalyzer.java @@ -26,8 +26,8 @@ import eu.melodic.event.brokercep.cep.MathUtil; import eu.melodic.event.translate.TranslationContext; import eu.melodic.event.translate.properties.CamelToEplTranslatorProperties; import eu.melodic.models.interfaces.ems.*; -import eu.passage.upperware.commons.model.tools.metadata.CamelMetadata; -import eu.passage.upperware.commons.model.tools.metadata.CamelMetadataTool; +import eu.melodic.event.translate.model.tools.metadata.CamelMetadata; +import eu.melodic.event.translate.model.tools.metadata.CamelMetadataTool; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.MapUtils; @@ -732,6 +732,9 @@ public class ModelAnalyzer { if (elseConstraint!=null) _TC.DAG.addNode(constraint, elseConstraint).setGrouping(getGrouping(elseConstraint)); + // cache constraint + _TC.addIfThenConstraint(constraint); + _decomposeConstraint(_TC, ifConstraint); _decomposeConstraint(_TC, thenConstraint); if (elseConstraint!=null) diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/generate/RuleGenerator.java b/event-management/translator/src/main/java/eu/melodic/event/translate/generate/RuleGenerator.java index e9d24b728d9027c16879bcf2ea89752bc833f7d6..316a501b59b45b9810e911f6973e0e084975c543 100644 --- a/event-management/translator/src/main/java/eu/melodic/event/translate/generate/RuleGenerator.java +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/generate/RuleGenerator.java @@ -26,7 +26,7 @@ import camel.scalability.UnaryEventPattern; import eu.melodic.event.brokercep.cep.MathUtil; import eu.melodic.event.translate.TranslationContext; import eu.melodic.event.translate.properties.RuleTemplateProperties; -import eu.passage.upperware.commons.model.tools.metadata.CamelMetadataTool; +import eu.melodic.event.translate.model.tools.metadata.CamelMetadataTool; import lombok.extern.slf4j.Slf4j; import org.eclipse.emf.common.util.EList; import org.springframework.beans.factory.annotation.Autowired; diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/model/tools/metadata/CamelMetadata.java b/event-management/translator/src/main/java/eu/melodic/event/translate/model/tools/metadata/CamelMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..e610e32f748f2b75f7ec3b2a5844e5afce4000d8 --- /dev/null +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/model/tools/metadata/CamelMetadata.java @@ -0,0 +1,35 @@ +// Copied from 'melodic-commons' +// Date: 2022-01-18 +package eu.melodic.event.translate.model.tools.metadata; + +import eu.paasage.upperware.metamodel.cp.VariableType; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum CamelMetadata { + + CORES("CPU", VariableType.CORES, false), + RAM("RAM", VariableType.RAM, false), + STORAGE("Storage", VariableType.STORAGE, false), + CARDINALITY("Cardinality", VariableType.CARDINALITY, false), + LATITUDE("latitude", VariableType.LATITUDE, false), + LONGITUDE("longitude", VariableType.LONGITUDE, false), + + PROVIDER("", VariableType.PROVIDER, false), + + PRICE("Cost", null, true), + UNMOVEABLE("Unmoveable", null, false); + + public String camelName; + public VariableType variableType; + public boolean onNodeCandidate; + + public static final List VM_LIST = Collections.unmodifiableList(Arrays.asList(CORES, RAM, STORAGE, CARDINALITY, LATITUDE, LONGITUDE)); + public static final List NC_LIST = Collections.unmodifiableList(Collections.singletonList(PRICE)); + +} diff --git a/event-management/translator/src/main/java/eu/melodic/event/translate/model/tools/metadata/CamelMetadataTool.java b/event-management/translator/src/main/java/eu/melodic/event/translate/model/tools/metadata/CamelMetadataTool.java new file mode 100644 index 0000000000000000000000000000000000000000..fef989aa8b9e655a8557f8d3848ee8b1285d3eb6 --- /dev/null +++ b/event-management/translator/src/main/java/eu/melodic/event/translate/model/tools/metadata/CamelMetadataTool.java @@ -0,0 +1,72 @@ +// Copied from 'melodic-commons' +// Date: 2022-01-18 +package eu.melodic.event.translate.model.tools.metadata; + +import camel.metric.impl.MetricVariableImpl; +import camel.mms.MmsObject; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.emf.common.util.EList; + +import java.util.List; +import java.util.Optional; + +@Slf4j +public class CamelMetadataTool { + + public static Optional findVariableFor(List variables, CamelMetadata camelMetadata) { + + return variables.stream() + .filter(variable -> !variable.isCurrentConfiguration()) + .filter(variable -> variable.getMetricTemplate() + .getAttribute() + .getAnnotations() + .stream() + .anyMatch(mmsObject -> camelMetadata.camelName.equals(mmsObject.getId()))) + .findFirst(); + } + + public static boolean isFromVariable(MetricVariableImpl metricVariable) { + return isVariableFromGroup(metricVariable, CamelMetadata.VM_LIST); + } + + public static boolean isFromNodeCandidate(MetricVariableImpl metricVariable) { + return isVariableFromGroup(metricVariable, CamelMetadata.NC_LIST); + } + + + public static CamelMetadata findVariableType(MetricVariableImpl metricVariable) { + return findCamelMetaDataType(metricVariable, CamelMetadata.VM_LIST); + } + + public static CamelMetadata findNodeCandidateAttributeType(MetricVariableImpl metricVariable) { + return findCamelMetaDataType(metricVariable, CamelMetadata.NC_LIST); + } + + + + private static CamelMetadata findCamelMetaDataType(MetricVariableImpl metricVariable, List metadataList) { + String annotation = getAnnotationOfMetricVariable(metricVariable); + return metadataList.stream().filter(type -> type.camelName.equals(annotation)).findAny().orElseThrow( + () -> new IllegalArgumentException("Wrong annotation: " + annotation + " is not a supported type")); + } + + private static boolean isVariableFromGroup(MetricVariableImpl metricVariable, List metadata) { + return metricVariable.getMetricTemplate().getAttribute().getAnnotations().stream().anyMatch(mmsObject -> checkAnnotation(mmsObject, metadata)); + } + + private static boolean checkAnnotation(MmsObject mmsObject, List metadata) { + return metadata.stream().anyMatch(camelMetadata -> camelMetadata.camelName.equals(mmsObject.getId())); + } + + private static String getAnnotationOfMetricVariable(MetricVariableImpl metricVariable) { + EList annotations = metricVariable.getMetricTemplate().getAttribute().getAnnotations(); + if (annotations.isEmpty()) { + log.warn("Metric Variable {} has not definied annotation, returning empty String", metricVariable.getName()); + return ""; + } + String annotation = annotations.get(0).getId(); + log.debug("Found annotation {} for metric: {}", metricVariable.getName(), annotation); + return annotation; + } + +} diff --git a/event-management/util/src/main/java/eu/melodic/event/util/ClientConfiguration.java b/event-management/util/src/main/java/eu/melodic/event/util/ClientConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..d3a1bc38c956e7d6f6b7410741c3d35173281958 --- /dev/null +++ b/event-management/util/src/main/java/eu/melodic/event/util/ClientConfiguration.java @@ -0,0 +1,26 @@ +/* + * 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/ + */ + +package eu.melodic.event.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/event-management/util/src/main/java/eu/melodic/event/util/CredentialsMap.java b/event-management/util/src/main/java/eu/melodic/event/util/CredentialsMap.java index 8bcd7eab97a0f0009bdf52e8ae2a3c73e39ccdec..9933f2f8faef0289b7852ca5a0ba7fb0f6b83d8b 100644 --- a/event-management/util/src/main/java/eu/melodic/event/util/CredentialsMap.java +++ b/event-management/util/src/main/java/eu/melodic/event/util/CredentialsMap.java @@ -9,7 +9,6 @@ package eu.melodic.event.util; -import eu.melodic.event.util.password.IdentityPasswordEncoder; import eu.melodic.event.util.password.PasswordEncoder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -18,16 +17,42 @@ import java.util.HashMap; import java.util.stream.Collectors; /** - * CredentialsMap is a HashMap with toString() method overidden in order to password encodes entry values. + * CredentialsMap is a HashMap with toString() method overridden in order to password encodes entry values. * Used to store credentials */ @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 CredentialsMap() { this(new IdentityPasswordEncoder()); } - public CredentialsMap(PasswordEncoder pe) { this.passwordEncoder = pe; } + 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() diff --git a/event-management/util/src/main/java/eu/melodic/event/util/EmsConstant.java b/event-management/util/src/main/java/eu/melodic/event/util/EmsConstant.java new file mode 100644 index 0000000000000000000000000000000000000000..7a5b0f98505ea9f605b8159e911b9a31fa8c7897 --- /dev/null +++ b/event-management/util/src/main/java/eu/melodic/event/util/EmsConstant.java @@ -0,0 +1,17 @@ +/* + * 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/ + */ + +package eu.melodic.event.util; + +/** + * EMS constant + */ +public class EmsConstant { + public final static String EVENT_PROPERTY_SOURCE_ADDRESS = "producer-host"; +} \ No newline at end of file diff --git a/event-management/util/src/main/java/eu/melodic/event/util/EventBus.java b/event-management/util/src/main/java/eu/melodic/event/util/EventBus.java new file mode 100644 index 0000000000000000000000000000000000000000..3ca2190637db0a3398d950d90d9eba5edf0e1744 --- /dev/null +++ b/event-management/util/src/main/java/eu/melodic/event/util/EventBus.java @@ -0,0 +1,146 @@ +/* + * 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/ + */ + +package eu.melodic.event.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/event-management/util/src/main/java/eu/melodic/event/util/GroupingConfiguration.java b/event-management/util/src/main/java/eu/melodic/event/util/GroupingConfiguration.java index 4575f348a5c458b24609734848e59610ebecf9e1..afb6b357dd2edf7a124a44875eb5e7fb8923c217 100644 --- a/event-management/util/src/main/java/eu/melodic/event/util/GroupingConfiguration.java +++ b/event-management/util/src/main/java/eu/melodic/event/util/GroupingConfiguration.java @@ -17,7 +17,7 @@ import java.util.Properties; import java.util.Set; /** - * Baguette Client Configuration + * Baguette Client Grouping Configuration */ @Data @Builder @@ -39,6 +39,7 @@ public class GroupingConfiguration implements Serializable { @Data @NoArgsConstructor @AllArgsConstructor + @ToString(exclude = {/*"certificate",*/ "password"}) public static class BrokerConnectionConfig implements Serializable { private String grouping; private String url; diff --git a/event-management/util/src/main/java/eu/melodic/event/util/KeystoreAndCertificateProperties.java b/event-management/util/src/main/java/eu/melodic/event/util/KeystoreAndCertificateProperties.java index 536e3dcfa613ae4fb4f1a2ff4a9ba8c762aa0ccf..01dbb855645d3b073895e52adacb4005dae9e0a5 100644 --- a/event-management/util/src/main/java/eu/melodic/event/util/KeystoreAndCertificateProperties.java +++ b/event-management/util/src/main/java/eu/melodic/event/util/KeystoreAndCertificateProperties.java @@ -14,9 +14,9 @@ import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +@Slf4j @Data @ToString(exclude = {"truststorePassword", "keystorePassword"}) -@Slf4j public class KeystoreAndCertificateProperties implements IKeystoreAndCertificateProperties { private String defaultIpAddress; @@ -53,14 +53,20 @@ public class KeystoreAndCertificateProperties implements IKeystoreAndCertificate public static String prepareValue(String value, String publicIpAddress, String defaultIpAddress, String defaultValue) { if (value==null) return null; - String pubIpAddr = NetUtil.getPublicIpAddress(); - pubIpAddr = StringUtils.isNotBlank(pubIpAddr) - ? pubIpAddr - : StringUtils.isNotBlank(publicIpAddress) ? publicIpAddress : defaultValue; - String defIpAddr = NetUtil.getDefaultIpAddress(); - defIpAddr = StringUtils.isNotBlank(defIpAddr) - ? defIpAddr - : StringUtils.isNotBlank(defaultIpAddress) ? defaultIpAddress: defaultValue; + String pubIpAddr = ""; + if (value.contains("%{PUBLIC_IP}%")) { + pubIpAddr = NetUtil.getPublicIpAddress(); + pubIpAddr = StringUtils.isNotBlank(pubIpAddr) + ? pubIpAddr + : StringUtils.isNotBlank(publicIpAddress) ? publicIpAddress : defaultValue; + } + String defIpAddr = ""; + if (value.contains("%{DEFAULT_IP}%")) { + defIpAddr = NetUtil.getDefaultIpAddress(); + defIpAddr = StringUtils.isNotBlank(defIpAddr) + ? defIpAddr + : StringUtils.isNotBlank(defaultIpAddress) ? defaultIpAddress : defaultValue; + } return value .replace("%{PUBLIC_IP}%", pubIpAddr) .replace("%{DEFAULT_IP}%", defIpAddr); diff --git a/event-management/util/src/main/java/eu/melodic/event/util/KeystoreUtil.java b/event-management/util/src/main/java/eu/melodic/event/util/KeystoreUtil.java index 0c574234805ff5b347e1f20ddd194495c97abb29..9d2eaedbf71145f9c35f9d1ff920eae01c141f1c 100644 --- a/event-management/util/src/main/java/eu/melodic/event/util/KeystoreUtil.java +++ b/event-management/util/src/main/java/eu/melodic/event/util/KeystoreUtil.java @@ -53,6 +53,7 @@ public class KeystoreUtil { private String keystoreFile; private String keystoreType; private String keystorePassword; + private PasswordUtil passwordUtil; // KeystoreUtil instance methods public static KeystoreUtil getKeystore(String file, String type, String password) { @@ -85,6 +86,17 @@ public class KeystoreUtil { return f.exists(); } + public PasswordUtil passwordUtil() { + if (this.passwordUtil==null) + this.passwordUtil = new PasswordUtil(); + return this.passwordUtil; + } + + public KeystoreUtil passwordUtil(PasswordUtil passwordUtil) { + this.passwordUtil = passwordUtil!=null ? passwordUtil : new PasswordUtil(); + 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 Exception { @@ -289,7 +301,7 @@ public class KeystoreUtil { // Certificate retrieval methods public X509Certificate getEntryCertificate(String entryName) throws Exception { log.trace("KeystoreUtil.getEntryCertificate(): keystore: file={}, type={}, password={}", - keystoreFile, keystoreType, keystorePassword); + keystoreFile, keystoreType, passwordUtil().encodePassword(keystorePassword)); KeyStore keystore = readKeystore(keystoreFile, keystoreType, keystorePassword); log.trace("KeystoreUtil.getEntryCertificate(): keystore: {}", keystore); log.trace("KeystoreUtil.getEntryCertificate(): entry-name: {}", entryName); @@ -312,9 +324,7 @@ public class KeystoreUtil { X509Certificate cert = getEntryCertificate(entryName); log.trace("KeystoreUtil.getEntryCertificatePEM(): X509 certificate:\n{}", cert); byte[] certBytes = cert.getEncoded(); - //if (log.isTraceEnabled()) { - log.debug("KeystoreUtil.getEntryCertificatePEM(): X509 certificate (DER):\n{}", certBytes); - //} + log.trace("KeystoreUtil.getEntryCertificatePEM(): X509 certificate (DER):\n{}", certBytes); return certBytes; } @@ -337,6 +347,7 @@ public class KeystoreUtil { : sanName; } catch (Exception ex) { log.warn("KeystoreUtil: getEntryNames: entry={} caused {}", sanName, ex.toString()); + log.debug("KeystoreUtil: getEntryNames: entry={} caused:\n", sanName, ex); return null; } }) @@ -411,12 +422,10 @@ public class KeystoreUtil { // Keystore, Trust store and Certificate initialization based on a properties source public static void initializeKeystoresAndCertificate(IKeystoreAndCertificateProperties properties, PasswordUtil passwordUtil) throws Exception { - String keystorePassword = properties.getKeystorePassword(); - String truststorePassword = properties.getTruststorePassword(); - if (passwordUtil!=null) { - keystorePassword = passwordUtil.encodePassword(keystorePassword); - truststorePassword = passwordUtil.encodePassword(truststorePassword); - } + if (passwordUtil==null) + passwordUtil = new PasswordUtil(); + String keystorePassword = passwordUtil.encodePassword(properties.getKeystorePassword()); + String truststorePassword = passwordUtil.encodePassword(properties.getTruststorePassword()); log.info("KeystoreUtil.initializeKeystoresAndCertificate(): Initializing keystores and certificate"); log.debug("KeystoreUtil.initializeKeystoresAndCertificate(): Key pair and Certificate settings:"); log.debug(" Keystore file: {}", properties.getKeystoreFile()); @@ -439,14 +448,17 @@ public class KeystoreUtil { // 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.getKeyEntryNameValue()); if (containsEntry) { log.debug(" Keystore already contains entry: {}", properties.getKeyEntryNameValue()); @@ -461,14 +473,17 @@ public class KeystoreUtil { // 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.getKeyEntryNameValue(), true); log.debug(" Entry addresses: {}", addrList); @@ -490,6 +505,7 @@ public class KeystoreUtil { KeystoreUtil ksUtil = KeystoreUtil .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) .createIfNotExist(); if (StringUtils.isBlank(properties.getKeyEntryExtSANValue())) { log.debug(" Create/Replace entry (with SAN auto-generate): {}", properties.getKeyEntryNameValue()); @@ -505,6 +521,7 @@ public class KeystoreUtil { 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.getKeyEntryNameValue(), properties.getCertificateFile()); @@ -518,6 +535,7 @@ public class KeystoreUtil { if (log.isDebugEnabled()) { String certPemStr = KeystoreUtil .getKeystore(properties.getKeystoreFile(), properties.getKeystoreType(), properties.getKeystorePassword()) + .passwordUtil(passwordUtil) .getEntryCertificateAsPEM(properties.getKeyEntryNameValue()); log.debug(" Certificate (PEM):\n{}", certPemStr); } diff --git a/event-management/util/src/main/java/eu/melodic/event/util/PasswordUtil.java b/event-management/util/src/main/java/eu/melodic/event/util/PasswordUtil.java index 95d262483172d230c32b60f4bb40f050d83005a7..b7a7e0c1b6b477d93a145a85a5f647b3f4b96ab6 100644 --- a/event-management/util/src/main/java/eu/melodic/event/util/PasswordUtil.java +++ b/event-management/util/src/main/java/eu/melodic/event/util/PasswordUtil.java @@ -9,7 +9,7 @@ package eu.melodic.event.util; -import eu.melodic.event.util.password.IdentityPasswordEncoder; +import eu.melodic.event.util.password.AsterisksPasswordEncoder; import eu.melodic.event.util.password.PasswordEncoder; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -17,21 +17,27 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -@Service @Slf4j +@Service public class PasswordUtil implements InitializingBean { - @Value("${password-encoder-class}") + private final static Supplier passwordEncoderSupplier = AsterisksPasswordEncoder::new; + private final static AtomicReference defaultPasswordEncoder = new AtomicReference<>(); + + @Value("${password-encoder-class:}") private String passwordEncoderClassName; private PasswordEncoder passwordEncoder; @Override public void afterPropertiesSet() { log.debug("PasswordUtil: password-encoder-class: {}", passwordEncoderClassName); - if (passwordEncoderClassName!=null) { - this.setPasswordEncoder(passwordEncoderClassName); - } + this.setPasswordEncoder(passwordEncoderClassName.trim()); + if (passwordEncoder!=null) + if (defaultPasswordEncoder.compareAndSet(null, passwordEncoder)) + log.info("PasswordUtil: Initialized default Password Encoder: {}", defaultPasswordEncoder.get().getClass().getName()); } public String encodePassword(String password) { @@ -39,7 +45,8 @@ public class PasswordUtil implements InitializingBean { } public PasswordEncoder getPasswordEncoder() { - return passwordEncoder; + return passwordEncoder!=null + ? passwordEncoder : (passwordEncoder=createPasswordEncoder(null)); } public void setPasswordEncoder(PasswordEncoder pe) { @@ -52,9 +59,8 @@ public class PasswordUtil implements InitializingBean { } public static PasswordEncoder createPasswordEncoder(String passwordEncoderClassName) { - Supplier passwordEncoderSupplier = IdentityPasswordEncoder::new; if (StringUtils.isBlank(passwordEncoderClassName)) { - log.info("Password encoder class name is empty. Default instance of PasswordEncoder will be created"); + log.warn("Password encoder class name is empty. Default instance of PasswordEncoder will be created"); return passwordEncoderSupplier.get(); } @@ -66,4 +72,9 @@ public class PasswordUtil implements InitializingBean { return passwordEncoderSupplier.get(); } } + + public static PasswordEncoder getDefaultPasswordEncoder() { + return Optional.ofNullable(defaultPasswordEncoder.get()) + .orElse(passwordEncoderSupplier.get()); + } } diff --git a/event-management/util/src/main/java/eu/melodic/event/util/Plugin.java b/event-management/util/src/main/java/eu/melodic/event/util/Plugin.java new file mode 100644 index 0000000000000000000000000000000000000000..4758aa59923efe9cf96fb21334941132b1f8dc15 --- /dev/null +++ b/event-management/util/src/main/java/eu/melodic/event/util/Plugin.java @@ -0,0 +1,15 @@ +/* + * 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/ + */ + +package eu.melodic.event.util; + +public interface Plugin { + void start(); + void stop(); +} diff --git a/event-management/web-admin/.dockerignore b/event-management/web-admin/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..7d2f2e85e308e2424d5ab95072e4c5f9b08e3cec --- /dev/null +++ b/event-management/web-admin/.dockerignore @@ -0,0 +1,3 @@ +.idea +dist +node_modules diff --git a/event-management/web-admin/.gitignore b/event-management/web-admin/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c382ea55c19fb05b0cb3be7aabdbc7234cad3499 --- /dev/null +++ b/event-management/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/event-management/web-admin/Dockerfile b/event-management/web-admin/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a1bd9b0fd99b2ed48ac0a80879d094685b67a412 --- /dev/null +++ b/event-management/web-admin/Dockerfile @@ -0,0 +1,25 @@ +# +# 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/ +# + +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/event-management/web-admin/README.md b/event-management/web-admin/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c47fbd8c83d7b21c75022593f19b686ecd805372 --- /dev/null +++ b/event-management/web-admin/README.md @@ -0,0 +1,24 @@ +# 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/event-management/web-admin/babel.config.js b/event-management/web-admin/babel.config.js new file mode 100644 index 0000000000000000000000000000000000000000..e9558405fdcc02f12d757acb308e02937a7444f1 --- /dev/null +++ b/event-management/web-admin/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/event-management/web-admin/package-lock.json b/event-management/web-admin/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..12714b8f613a56d2b7c267ed6237d182324ead23 --- /dev/null +++ b/event-management/web-admin/package-lock.json @@ -0,0 +1,12480 @@ +{ + "name": "ems-web-admin", + "version": "0.9.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true + }, + "@babel/core": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", + "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.0", + "@babel/helper-module-transforms": "^7.15.0", + "@babel/helpers": "^7.14.8", + "@babel/parser": "^7.15.0", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", + "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", + "dev": true, + "requires": { + "@babel/types": "^7.15.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", + "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz", + "integrity": "sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz", + "integrity": "sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.0.tgz", + "integrity": "sha512-MdmDXgvTIi4heDVX/e9EFfeGpugqm9fobBVg/iioE8kueXrOHdRDe36FAY7SnE9xXLVeYCoJR/gdrBEIHRC83Q==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-member-expression-to-functions": "^7.15.0", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.0", + "@babel/helper-split-export-declaration": "^7.14.5" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", + "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz", + "integrity": "sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz", + "integrity": "sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", + "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz", + "integrity": "sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg==", + "dev": true, + "requires": { + "@babel/types": "^7.15.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", + "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz", + "integrity": "sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.0", + "@babel/helper-simple-access": "^7.14.8", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.9", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", + "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz", + "integrity": "sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-wrap-function": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz", + "integrity": "sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.15.0", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz", + "integrity": "sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==", + "dev": true, + "requires": { + "@babel/types": "^7.14.8" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz", + "integrity": "sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==" + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz", + "integrity": "sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helpers": { + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.8.tgz", + "integrity": "sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw==", + "dev": true, + "requires": { + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.8", + "@babel/types": "^7.14.8" + } + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.15.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.2.tgz", + "integrity": "sha512-bMJXql1Ss8lFnvr11TZDH4ArtwlAS5NG9qBmdiFW2UHHm6MVoR+GDc5XE2b9K938cyjc9O6/+vjjcffLDtfuDg==" + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz", + "integrity": "sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz", + "integrity": "sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz", + "integrity": "sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz", + "integrity": "sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.14.5.tgz", + "integrity": "sha512-LYz5nvQcvYeRVjui1Ykn28i+3aUiXwQ/3MGoEy0InTaz1pJo/lAzmIDXX+BQny/oufgHzJ6vnEEiXQ8KZjEVFg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-decorators": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz", + "integrity": "sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz", + "integrity": "sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz", + "integrity": "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz", + "integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz", + "integrity": "sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz", + "integrity": "sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz", + "integrity": "sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.14.7", + "@babel/helper-compilation-targets": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.14.5" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz", + "integrity": "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz", + "integrity": "sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz", + "integrity": "sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz", + "integrity": "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.14.5.tgz", + "integrity": "sha512-c4sZMRWL4GSvP1EXy0woIP7m4jkVcEuG8R1TOZxPBPtp4FSM/kiPZub9UIs/Jrb5ZAOzvTUSGYrWsrSu1JvoPw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", + "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz", + "integrity": "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz", + "integrity": "sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz", + "integrity": "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz", + "integrity": "sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz", + "integrity": "sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz", + "integrity": "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", + "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz", + "integrity": "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz", + "integrity": "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz", + "integrity": "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz", + "integrity": "sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz", + "integrity": "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz", + "integrity": "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz", + "integrity": "sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz", + "integrity": "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.0.tgz", + "integrity": "sha512-3H/R9s8cXcOGE8kgMlmjYYC9nqr5ELiPkJn4q0mypBrjhYQoc+5/Maq69vV4xRPWnkzZuwJPf5rArxpB/35Cig==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.15.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-simple-access": "^7.14.8", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz", + "integrity": "sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", + "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz", + "integrity": "sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", + "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", + "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz", + "integrity": "sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", + "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz", + "integrity": "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz", + "integrity": "sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.15.0.tgz", + "integrity": "sha512-sfHYkLGjhzWTq6xsuQ01oEsUYjkHRux9fW1iUA68dC7Qd8BS1Unq4aZ8itmQp95zUzIcyR2EbNMTzAicFj+guw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.2", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "semver": "^6.3.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz", + "integrity": "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz", + "integrity": "sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz", + "integrity": "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz", + "integrity": "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz", + "integrity": "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz", + "integrity": "sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz", + "integrity": "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/preset-env": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.15.0.tgz", + "integrity": "sha512-FhEpCNFCcWW3iZLg0L2NPE9UerdtsCR6ZcsGHUX6Om6kbCQeL5QZDqFDmeNHC6/fy6UH3jEge7K4qG5uC9In0Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-async-generator-functions": "^7.14.9", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-class-static-block": "^7.14.5", + "@babel/plugin-proposal-dynamic-import": "^7.14.5", + "@babel/plugin-proposal-export-namespace-from": "^7.14.5", + "@babel/plugin-proposal-json-strings": "^7.14.5", + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", + "@babel/plugin-proposal-numeric-separator": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-private-methods": "^7.14.5", + "@babel/plugin-proposal-private-property-in-object": "^7.14.5", + "@babel/plugin-proposal-unicode-property-regex": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.14.5", + "@babel/plugin-transform-async-to-generator": "^7.14.5", + "@babel/plugin-transform-block-scoped-functions": "^7.14.5", + "@babel/plugin-transform-block-scoping": "^7.14.5", + "@babel/plugin-transform-classes": "^7.14.9", + "@babel/plugin-transform-computed-properties": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.7", + "@babel/plugin-transform-dotall-regex": "^7.14.5", + "@babel/plugin-transform-duplicate-keys": "^7.14.5", + "@babel/plugin-transform-exponentiation-operator": "^7.14.5", + "@babel/plugin-transform-for-of": "^7.14.5", + "@babel/plugin-transform-function-name": "^7.14.5", + "@babel/plugin-transform-literals": "^7.14.5", + "@babel/plugin-transform-member-expression-literals": "^7.14.5", + "@babel/plugin-transform-modules-amd": "^7.14.5", + "@babel/plugin-transform-modules-commonjs": "^7.15.0", + "@babel/plugin-transform-modules-systemjs": "^7.14.5", + "@babel/plugin-transform-modules-umd": "^7.14.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.9", + "@babel/plugin-transform-new-target": "^7.14.5", + "@babel/plugin-transform-object-super": "^7.14.5", + "@babel/plugin-transform-parameters": "^7.14.5", + "@babel/plugin-transform-property-literals": "^7.14.5", + "@babel/plugin-transform-regenerator": "^7.14.5", + "@babel/plugin-transform-reserved-words": "^7.14.5", + "@babel/plugin-transform-shorthand-properties": "^7.14.5", + "@babel/plugin-transform-spread": "^7.14.6", + "@babel/plugin-transform-sticky-regex": "^7.14.5", + "@babel/plugin-transform-template-literals": "^7.14.5", + "@babel/plugin-transform-typeof-symbol": "^7.14.5", + "@babel/plugin-transform-unicode-escapes": "^7.14.5", + "@babel/plugin-transform-unicode-regex": "^7.14.5", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.15.0", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.2", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "core-js-compat": "^3.16.0", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz", + "integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/traverse": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", + "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.0", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.15.0", + "@babel/types": "^7.15.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", + "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + }, + "@hapi/address": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", + "dev": true + }, + "@hapi/bourne": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", + "dev": true + }, + "@hapi/hoek": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", + "dev": true + }, + "@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "dev": true, + "requires": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "dev": true, + "requires": { + "@hapi/hoek": "^8.3.0" + } + }, + "@intervolga/optimize-cssnano-plugin": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz", + "integrity": "sha512-zN69TnSr0viRSU6cEDIcuPcP67QcpQ6uHACg58FiN9PDrU6SLyGW3MR4tiISbYxy1kDWAVPwD+XwQTWE5cigAA==", + "dev": true, + "requires": { + "cssnano": "^4.0.0", + "cssnano-preset-default": "^4.0.0", + "postcss": "^7.0.0" + } + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, + "@soda/friendly-errors-webpack-plugin": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz", + "integrity": "sha512-RLotfx6k1+nfLacwNCenj7VnTMPxVwYKoGOcffMFoJDKM8tXzBiCN0hMHFJNnoAojduYAsxuiMm0EOMixgiRow==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "error-stack-parser": "^2.0.2", + "string-width": "^2.0.0", + "strip-ansi": "^5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + } + } + }, + "@soda/get-current-script": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@soda/get-current-script/-/get-current-script-1.0.2.tgz", + "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", + "dev": true + }, + "@tip2tail/jqvmap": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@tip2tail/jqvmap/-/jqvmap-1.6.0.tgz", + "integrity": "sha512-oqcgb8JMNO1+ixej95sbt7jMz1BOQ98IMmz6Fi/zWPSC/AkDhvudCaAswfa6ct+e/GmNcpnOWT/sJQ9ZXyNxDg==", + "requires": { + "jquery": "~3.3.1" + }, + "dependencies": { + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + } + } + }, + "@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/http-proxy": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", + "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "@types/node": { + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==", + "dev": true + }, + "@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "@types/q": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", + "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", + "dev": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", + "integrity": "sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack": { + "version": "4.41.30", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.30.tgz", + "integrity": "sha512-GUHyY+pfuQ6haAfzu4S14F+R5iGRwN6b2FRNJY7U0NilmFAqbsOfK6j1HwuLBAqwRIT+pVdNDJGJ6e8rpp0KHA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack-dev-server": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.5.tgz", + "integrity": "sha512-vjsbQBW3fE5FDICkF3w3ZWFRXNwQdKt7JRPLmRy5W0KXlcuew4wgpKWXhgHS71iLNv7Z2PlY9dSSIaYg+bk+9w==", + "dev": true, + "requires": { + "@types/connect-history-api-fallback": "*", + "@types/express": "*", + "@types/serve-static": "*", + "@types/webpack": "^4", + "http-proxy-middleware": "^1.0.0" + } + }, + "@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "@vue/babel-helper-vue-jsx-merge-props": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz", + "integrity": "sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==", + "dev": true + }, + "@vue/babel-helper-vue-transform-on": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz", + "integrity": "sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==", + "dev": true + }, + "@vue/babel-plugin-jsx": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.0.6.tgz", + "integrity": "sha512-RzYsvBhzKUmY2YG6LoV+W5PnlnkInq0thh1AzCmewwctAgGN6e9UFon6ZrQQV1CO5G5PeME7MqpB+/vvGg0h4g==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "@vue/babel-helper-vue-transform-on": "^1.0.2", + "camelcase": "^6.0.0", + "html-tags": "^3.1.0", + "svg-tags": "^1.0.0" + } + }, + "@vue/babel-plugin-transform-vue-jsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.2.1.tgz", + "integrity": "sha512-HJuqwACYehQwh1fNT8f4kyzqlNMpBuUK4rSiSES5D4QsYncv5fxFsLyrxFPG2ksO7t5WP+Vgix6tt6yKClwPzA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "html-tags": "^2.0.0", + "lodash.kebabcase": "^4.1.1", + "svg-tags": "^1.0.0" + }, + "dependencies": { + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=", + "dev": true + } + } + }, + "@vue/babel-preset-app": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/babel-preset-app/-/babel-preset-app-4.5.13.tgz", + "integrity": "sha512-pM7CR3yXB6L8Gfn6EmX7FLNE3+V/15I3o33GkSNsWvgsMp6HVGXKkXgojrcfUUauyL1LZOdvTmu4enU2RePGHw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.0", + "@babel/helper-compilation-targets": "^7.9.6", + "@babel/helper-module-imports": "^7.8.3", + "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-proposal-decorators": "^7.8.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-jsx": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.11.0", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.0", + "@vue/babel-plugin-jsx": "^1.0.3", + "@vue/babel-preset-jsx": "^1.2.4", + "babel-plugin-dynamic-import-node": "^2.3.3", + "core-js": "^3.6.5", + "core-js-compat": "^3.6.5", + "semver": "^6.1.0" + } + }, + "@vue/babel-preset-jsx": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.2.4.tgz", + "integrity": "sha512-oRVnmN2a77bYDJzeGSt92AuHXbkIxbf/XXSE3klINnh9AXBmVS1DGa1f0d+dDYpLfsAKElMnqKTQfKn7obcL4w==", + "dev": true, + "requires": { + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-plugin-transform-vue-jsx": "^1.2.1", + "@vue/babel-sugar-composition-api-inject-h": "^1.2.1", + "@vue/babel-sugar-composition-api-render-instance": "^1.2.4", + "@vue/babel-sugar-functional-vue": "^1.2.2", + "@vue/babel-sugar-inject-h": "^1.2.2", + "@vue/babel-sugar-v-model": "^1.2.3", + "@vue/babel-sugar-v-on": "^1.2.3" + } + }, + "@vue/babel-sugar-composition-api-inject-h": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.2.1.tgz", + "integrity": "sha512-4B3L5Z2G+7s+9Bwbf+zPIifkFNcKth7fQwekVbnOA3cr3Pq71q71goWr97sk4/yyzH8phfe5ODVzEjX7HU7ItQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-composition-api-render-instance": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.2.4.tgz", + "integrity": "sha512-joha4PZznQMsxQYXtR3MnTgCASC9u3zt9KfBxIeuI5g2gscpTsSKRDzWQt4aqNIpx6cv8On7/m6zmmovlNsG7Q==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-functional-vue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.2.2.tgz", + "integrity": "sha512-JvbgGn1bjCLByIAU1VOoepHQ1vFsroSA/QkzdiSs657V79q6OwEWLCQtQnEXD/rLTA8rRit4rMOhFpbjRFm82w==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-inject-h": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.2.2.tgz", + "integrity": "sha512-y8vTo00oRkzQTgufeotjCLPAvlhnpSkcHFEp60+LJUwygGcd5Chrpn5480AQp/thrxVm8m2ifAk0LyFel9oCnw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-v-model": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.2.3.tgz", + "integrity": "sha512-A2jxx87mySr/ulAsSSyYE8un6SIH0NWHiLaCWpodPCVOlQVODCaSpiR4+IMsmBr73haG+oeCuSvMOM+ttWUqRQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-plugin-transform-vue-jsx": "^1.2.1", + "camelcase": "^5.0.0", + "html-tags": "^2.0.0", + "svg-tags": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=", + "dev": true + } + } + }, + "@vue/babel-sugar-v-on": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.2.3.tgz", + "integrity": "sha512-kt12VJdz/37D3N3eglBywV8GStKNUhNrsxChXIV+o0MwVXORYuhDTHJRKPgLJRb/EY3vM2aRFQdxJBp9CLikjw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.2.1", + "camelcase": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "@vue/cli-overlay": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.13.tgz", + "integrity": "sha512-jhUIg3klgi5Cxhs8dnat5hi/W2tQJvsqCxR0u6hgfSob0ORODgUBlN+F/uwq7cKIe/pzedVUk1y07F13GQvPqg==", + "dev": true + }, + "@vue/cli-plugin-babel": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-babel/-/cli-plugin-babel-4.5.13.tgz", + "integrity": "sha512-ykvEAfD8PgGs+dGMGqr7l/nRmIS39NRzWLhMluPLTvDV1L+IxcoB73HNLGA/aENDpl8CuWrTE+1VgydcOhp+wg==", + "dev": true, + "requires": { + "@babel/core": "^7.11.0", + "@vue/babel-preset-app": "^4.5.13", + "@vue/cli-shared-utils": "^4.5.13", + "babel-loader": "^8.1.0", + "cache-loader": "^4.1.0", + "thread-loader": "^2.1.3", + "webpack": "^4.0.0" + } + }, + "@vue/cli-plugin-eslint": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.13.tgz", + "integrity": "sha512-yc2uXX6aBiy3vEf5TwaueaDqQbdIXIhk0x0KzEtpPo23jBdLkpOSoU5NCgE06g/ZiGAcettpmBSv73Hfp4wHEw==", + "dev": true, + "requires": { + "@vue/cli-shared-utils": "^4.5.13", + "eslint-loader": "^2.2.1", + "globby": "^9.2.0", + "inquirer": "^7.1.0", + "webpack": "^4.0.0", + "yorkie": "^2.0.0" + } + }, + "@vue/cli-plugin-router": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.13.tgz", + "integrity": "sha512-tgtMDjchB/M1z8BcfV4jSOY9fZSMDTPgF9lsJIiqBWMxvBIsk9uIZHxp62DibYME4CCKb/nNK61XHaikFp+83w==", + "dev": true, + "requires": { + "@vue/cli-shared-utils": "^4.5.13" + } + }, + "@vue/cli-plugin-vuex": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.13.tgz", + "integrity": "sha512-I1S9wZC7iI0Wn8kw8Zh+A2Qkf6s1M6vTGBkx8boXjuzfwEEyEHRxadsVCecZc8Mkpydo0nykj+MyYF96TKFuVA==", + "dev": true + }, + "@vue/cli-service": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.13.tgz", + "integrity": "sha512-CKAZN4iokMMsaUyJRU22oUAz3oS/X9sVBSKAF2/shFBV5xh3jqAlKl8OXZYz4cXGFLA6djNuYrniuLAo7Ku97A==", + "dev": true, + "requires": { + "@intervolga/optimize-cssnano-plugin": "^1.0.5", + "@soda/friendly-errors-webpack-plugin": "^1.7.1", + "@soda/get-current-script": "^1.0.0", + "@types/minimist": "^1.2.0", + "@types/webpack": "^4.0.0", + "@types/webpack-dev-server": "^3.11.0", + "@vue/cli-overlay": "^4.5.13", + "@vue/cli-plugin-router": "^4.5.13", + "@vue/cli-plugin-vuex": "^4.5.13", + "@vue/cli-shared-utils": "^4.5.13", + "@vue/component-compiler-utils": "^3.1.2", + "@vue/preload-webpack-plugin": "^1.1.0", + "@vue/web-component-wrapper": "^1.2.0", + "acorn": "^7.4.0", + "acorn-walk": "^7.1.1", + "address": "^1.1.2", + "autoprefixer": "^9.8.6", + "browserslist": "^4.12.0", + "cache-loader": "^4.1.0", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "cli-highlight": "^2.1.4", + "clipboardy": "^2.3.0", + "cliui": "^6.0.0", + "copy-webpack-plugin": "^5.1.1", + "css-loader": "^3.5.3", + "cssnano": "^4.1.10", + "debug": "^4.1.1", + "default-gateway": "^5.0.5", + "dotenv": "^8.2.0", + "dotenv-expand": "^5.1.0", + "file-loader": "^4.2.0", + "fs-extra": "^7.0.1", + "globby": "^9.2.0", + "hash-sum": "^2.0.0", + "html-webpack-plugin": "^3.2.0", + "launch-editor-middleware": "^2.2.1", + "lodash.defaultsdeep": "^4.6.1", + "lodash.mapvalues": "^4.6.0", + "lodash.transform": "^4.6.0", + "mini-css-extract-plugin": "^0.9.0", + "minimist": "^1.2.5", + "pnp-webpack-plugin": "^1.6.4", + "portfinder": "^1.0.26", + "postcss-loader": "^3.0.0", + "ssri": "^8.0.1", + "terser-webpack-plugin": "^1.4.4", + "thread-loader": "^2.1.3", + "url-loader": "^2.2.0", + "vue-loader": "^15.9.2", + "vue-loader-v16": "npm:vue-loader@^16.1.0", + "vue-style-loader": "^4.1.2", + "webpack": "^4.0.0", + "webpack-bundle-analyzer": "^3.8.0", + "webpack-chain": "^6.4.0", + "webpack-dev-server": "^3.11.0", + "webpack-merge": "^4.2.2" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + } + } + }, + "@vue/cli-shared-utils": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.13.tgz", + "integrity": "sha512-HpnOrkLg42RFUsQGMJv26oTG3J3FmKtO2WSRhKIIL+1ok3w9OjGCtA3nMMXN27f9eX14TqO64M36DaiSZ1fSiw==", + "dev": true, + "requires": { + "@hapi/joi": "^15.0.1", + "chalk": "^2.4.2", + "execa": "^1.0.0", + "launch-editor": "^2.2.1", + "lru-cache": "^5.1.1", + "node-ipc": "^9.1.1", + "open": "^6.3.0", + "ora": "^3.4.0", + "read-pkg": "^5.1.1", + "request": "^2.88.2", + "semver": "^6.1.0", + "strip-ansi": "^6.0.0" + } + }, + "@vue/compiler-core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.5.tgz", + "integrity": "sha512-TXBhFinoBaXKDykJzY26UEuQU1K07FOp/0Ie+OXySqqk0bS0ZO7Xvl7UmiTUPYcLrWbxWBR7Bs/y55AI0MNc2Q==", + "requires": { + "@babel/parser": "^7.12.0", + "@babel/types": "^7.12.0", + "@vue/shared": "3.1.5", + "estree-walker": "^2.0.1", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "@vue/compiler-dom": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.5.tgz", + "integrity": "sha512-ZsL3jqJ52OjGU/YiT/9XiuZAmWClKInZM2aFJh9gnsAPqOrj2JIELMbkIFpVKR/CrVO/f2VxfPiiQdQTr65jcQ==", + "requires": { + "@vue/compiler-core": "3.1.5", + "@vue/shared": "3.1.5" + } + }, + "@vue/compiler-sfc": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.5.tgz", + "integrity": "sha512-mtMY6xMvZeSRx9MTa1+NgJWndrkzVTdJ1pQAmAKQuxyb5LsHVvrgP7kcQFvxPHVpLVTORbTJWHaiqoKrJvi1iA==", + "dev": true, + "requires": { + "@babel/parser": "^7.13.9", + "@babel/types": "^7.13.0", + "@types/estree": "^0.0.48", + "@vue/compiler-core": "3.1.5", + "@vue/compiler-dom": "3.1.5", + "@vue/compiler-ssr": "3.1.5", + "@vue/shared": "3.1.5", + "consolidate": "^0.16.0", + "estree-walker": "^2.0.1", + "hash-sum": "^2.0.0", + "lru-cache": "^5.1.1", + "magic-string": "^0.25.7", + "merge-source-map": "^1.1.0", + "postcss": "^8.1.10", + "postcss-modules": "^4.0.0", + "postcss-selector-parser": "^6.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "consolidate": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz", + "integrity": "sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==", + "dev": true, + "requires": { + "bluebird": "^3.7.2" + } + }, + "postcss": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", + "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map-js": "^0.6.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@vue/compiler-ssr": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.5.tgz", + "integrity": "sha512-CU5N7Di/a4lyJ18LGJxJYZS2a8PlLdWpWHX9p/XcsjT2TngMpj3QvHVRkuik2u8QrIDZ8OpYmTyj1WDNsOV+Dg==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.1.5", + "@vue/shared": "3.1.5" + } + }, + "@vue/component-compiler-utils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.2.2.tgz", + "integrity": "sha512-rAYMLmgMuqJFWAOb3Awjqqv5X3Q3hVr4jH/kgrFJpiU0j3a90tnNBplqbj+snzrgZhC9W128z+dtgMifOiMfJg==", + "dev": true, + "requires": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.36", + "postcss-selector-parser": "^6.0.2", + "prettier": "^1.18.2", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "@vue/devtools-api": { + "version": "6.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.15.tgz", + "integrity": "sha512-quBx4Jjpexo6KDiNUGFr/zF/2A4srKM9S9v2uHgMXSU//hjgq1eGzqkIFql8T9gfX5ZaVOUzYBP3jIdIR3PKIA==" + }, + "@vue/preload-webpack-plugin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz", + "integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==", + "dev": true + }, + "@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "requires": { + "@vue/shared": "3.1.5" + } + }, + "@vue/runtime-core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.5.tgz", + "integrity": "sha512-YQbG5cBktN1RowQDKA22itmvQ+b40f0WgQ6CXK4VYoYICAiAfu6Cc14777ve8zp1rJRGtk5oIeS149TOculrTg==", + "requires": { + "@vue/reactivity": "3.1.5", + "@vue/shared": "3.1.5" + } + }, + "@vue/runtime-dom": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.5.tgz", + "integrity": "sha512-tNcf3JhVR0RfW0kw1p8xZgv30nvX8Y9rsz7eiQ0dHe273sfoCngAG0y4GvMaY4Xd8FsjUwFedd4suQ8Lu8meXg==", + "requires": { + "@vue/runtime-core": "3.1.5", + "@vue/shared": "3.1.5", + "csstype": "^2.6.8" + } + }, + "@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" + }, + "@vue/web-component-wrapper": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", + "integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "address": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", + "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", + "dev": true, + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, + "babel-loader": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", + "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz", + "integrity": "sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.2", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.4.tgz", + "integrity": "sha512-z3HnJE5TY/j4EFEa/qpQMSbcUJZ5JQi+3UFjXzn6pQCmIKc5Ug5j98SuYyH+m4xQnvKlMDIW4plLfgyVnd0IcQ==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.2", + "core-js-compat": "^3.14.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz", + "integrity": "sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bfj": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", + "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "check-types": "^8.0.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + }, + "dependencies": { + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dev": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.16.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.7.tgz", + "integrity": "sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001248", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.793", + "escalade": "^3.1.1", + "node-releases": "^1.1.73" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-json/-/buffer-json-2.0.0.tgz", + "integrity": "sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cache-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-4.1.0.tgz", + "integrity": "sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==", + "dev": true, + "requires": { + "buffer-json": "^2.0.0", + "find-cache-dir": "^3.0.0", + "loader-utils": "^1.2.3", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "schema-utils": "^2.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001300", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz", + "integrity": "sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==", + "dev": true + }, + "case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chart.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz", + "integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA==" + }, + "check-types": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==", + "dev": true + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "chroma-js": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-1.4.1.tgz", + "integrity": "sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ==" + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cli-spinners": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz", + "integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "dev": true, + "requires": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "dev": true, + "requires": { + "bluebird": "^3.1.1" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz", + "integrity": "sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ==", + "dev": true, + "requires": { + "cacache": "^12.0.3", + "find-cache-dir": "^2.1.0", + "glob-parent": "^3.1.0", + "globby": "^7.1.1", + "is-glob": "^4.0.1", + "loader-utils": "^1.2.3", + "minimatch": "^3.0.4", + "normalize-path": "^3.0.0", + "p-limit": "^2.2.1", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + } + } + }, + "core-js": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.1.tgz", + "integrity": "sha512-AAkP8i35EbefU+JddyWi12AWE9f2N/qr/pwnDtWz4nyUIBGMJPX99ANFFRSw6FefM374lDujdtLDyhN2A/btHw==" + }, + "core-js-compat": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.16.1.tgz", + "integrity": "sha512-NHXQXvRbd4nxp9TEmooTJLUf94ySUG6+DSsscBpTftN1lQLQ4LjnWvc7AoIo4UjDsFF3hB8Uh5LLCRRdaiT5MQ==", + "dev": true, + "requires": { + "browserslist": "^4.16.7", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-loader": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", + "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^6.3.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", + "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.8", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", + "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.3", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "requires": { + "css-tree": "^1.1.2" + }, + "dependencies": { + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "csstype": { + "version": "2.6.17", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", + "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==" + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true + }, + "default-gateway": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-5.0.5.tgz", + "integrity": "sha512-z2RnruVmj8hVMmAnEJMTIJNijhKCDiGjbLP+BHJFOT7ld3Bo5qcIBpVYDniqhbMIIf+jZDlkP2MkPXiQy/DBLA==", + "dev": true, + "requires": { + "execa": "^3.3.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + } + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.799", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.799.tgz", + "integrity": "sha512-V2rbYWdGvSqrg+95KjkVuSi41bGfrhrOzjl1tSi2VLnm0mRe3FsSvhiqidSiSll9WiMhrQAhpDcW/wcqK3c+Yw==", + "dev": true + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dev": true, + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-stack-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz", + "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", + "dev": true, + "requires": { + "stackframe": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", + "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "eslint-loader": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.2.1.tgz", + "integrity": "sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg==", + "dev": true, + "requires": { + "loader-fs-cache": "^1.0.0", + "loader-utils": "^1.0.2", + "object-assign": "^4.0.1", + "object-hash": "^1.1.4", + "rimraf": "^2.6.1" + } + }, + "eslint-plugin-vue": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.15.1.tgz", + "integrity": "sha512-4/r+n/i+ovyeW2gVRRH92kpy4lkpFbyPR4BMxGBTLtGnwqOKKzjSo6EMSaT0RhWPvEjK9uifcY8e7z5n8BIEgw==", + "dev": true, + "requires": { + "eslint-utils": "^2.1.0", + "natural-compare": "^1.4.0", + "semver": "^6.3.0", + "vue-eslint-parser": "^7.10.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "eventsource": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", + "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.5.0" + } + }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "generic-names": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz", + "integrity": "sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "dev": true + }, + "html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "dependencies": { + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + } + } + }, + "html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "dependencies": { + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + } + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", + "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.5", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + }, + "dependencies": { + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + } + } + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-bigint": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.3.tgz", + "integrity": "sha512-ZU538ajmYJmzysE5yU4Y7uIrPQ2j704u+hXFiIPQExpqzzUbpe5jCPdTfmz7jXRxZdvjY3KZ3ZNenoXQovX+Dg==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.5.0.tgz", + "integrity": "sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA