diff --git a/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md b/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md deleted file mode 100644 index d2a5bad26b..0000000000 --- a/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md +++ /dev/null @@ -1,3 +0,0 @@ -###### WARNING NOTE: 'localhost' can not be used as CLOUD_RPC_HOST - -Please note that your ThingsBoard base URL is **'localhost'** at the moment. **'localhost'** cannot be used for docker containers - please update **CLOUD_RPC_HOST** environment variable below to the IP address of your machine (*docker **host** machine*). IP address must be `192.168.1.XX` or similar format. In other case - ThingsBoard Edge service, that is running in docker container, will not be able to connect to the cloud. diff --git a/application/src/main/data/json/edge/install_instructions/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md similarity index 91% rename from application/src/main/data/json/edge/install_instructions/centos/instructions.md rename to application/src/main/data/json/edge/instructions/install/centos/instructions.md index 126c79b641..22bb3f63a0 100644 --- a/application/src/main/data/json/edge/install_instructions/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/centos/instructions.md @@ -1,4 +1,4 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the cloud. +Here is the list of commands, that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server. #### Prerequisites Before continue to installation execute the following commands in order to install necessary tools: @@ -56,13 +56,13 @@ sudo yum update sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm # Install packages sudo yum -y install epel-release yum-utils -sudo yum-config-manager --enable pgdg12 -sudo yum install postgresql12-server postgresql12 +sudo yum-config-manager --enable pgdg15 +sudo yum install postgresql15-server postgresql15 # Initialize your PostgreSQL DB -sudo /usr/pgsql-12/bin/postgresql-12-setup initdb -sudo systemctl start postgresql-12 +sudo /usr/pgsql-15/bin/postgresql-15-setup initdb +sudo systemctl start postgresql-15 # Optional: Configure PostgreSQL to start on boot -sudo systemctl enable --now postgresql-12 +sudo systemctl enable --now postgresql-15 {:copy-code} ``` @@ -74,12 +74,12 @@ sudo systemctl enable --now postgresql-12 sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm # Install packages sudo dnf -qy module disable postgresql -sudo dnf -y install postgresql12 postgresql12-server +sudo dnf -y install postgresql15 postgresql15-server # Initialize your PostgreSQL DB -sudo /usr/pgsql-12/bin/postgresql-12-setup initdb -sudo systemctl start postgresql-12 +sudo /usr/pgsql-15/bin/postgresql-15-setup initdb +sudo systemctl start postgresql-15 # Optional: Configure PostgreSQL to start on boot -sudo systemctl enable --now postgresql-12 +sudo systemctl enable --now postgresql-15 {:copy-code} ``` @@ -101,7 +101,7 @@ After configuring the password, edit the pg_hba.conf to use MD5 authentication w Edit pg_hba.conf file: ```bash -sudo nano /var/lib/pgsql/12/data/pg_hba.conf +sudo nano /var/lib/pgsql/15/data/pg_hba.conf {:copy-code} ``` @@ -121,7 +121,7 @@ host all all 127.0.0.1/32 md5 Finally, you should restart the PostgreSQL service to initialize the new configuration: ```bash -sudo systemctl restart postgresql-12.service +sudo systemctl restart postgresql-15.service {:copy-code} ``` diff --git a/application/src/main/data/json/edge/install_instructions/docker/instructions.md b/application/src/main/data/json/edge/instructions/install/docker/instructions.md similarity index 77% rename from application/src/main/data/json/edge/install_instructions/docker/instructions.md rename to application/src/main/data/json/edge/instructions/install/docker/instructions.md index d8f9731889..9910124b3e 100644 --- a/application/src/main/data/json/edge/install_instructions/docker/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/docker/instructions.md @@ -1,25 +1,11 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge using docker compose and connect to the cloud. +Here is the list of commands, that can be used to quickly install ThingsBoard Edge using docker compose and connect to the server. #### Prerequisites Install Docker CE and Docker Compose. -#### Create data and logs folders - -Run following commands, before starting docker container(s), to create folders for storing data and logs. -These commands additionally will change owner of newly created folders to docker container user. -To do this (to change user) **chown** command is used, and this command requires *sudo* permissions (command will request password for a *sudo* access): - -```bash -mkdir -p ~/.mytb-edge-data && sudo chown -R 799:799 ~/.mytb-edge-data -mkdir -p ~/.mytb-edge-logs && sudo chown -R 799:799 ~/.mytb-edge-logs -{:copy-code} -``` - #### Running ThingsBoard Edge as docker service -${LOCALHOST_WARNING} - Create docker compose file for ThingsBoard Edge service: ```bash @@ -30,7 +16,7 @@ nano docker-compose.yml Add the following lines to the yml file: ```bash -version: '3.0' +version: '3.8' services: mytbedge: restart: always @@ -47,8 +33,9 @@ services: CLOUD_RPC_PORT: ${CLOUD_RPC_PORT} CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED} volumes: - - ~/.mytb-edge-data:/data - - ~/.mytb-edge-logs:/var/log/tb-edge + - tb-edge-data:/data + - tb-edge-logs:/var/log/tb-edge + ${EXTRA_HOSTS} postgres: restart: always image: "postgres:15" @@ -58,7 +45,15 @@ services: POSTGRES_DB: tb-edge POSTGRES_PASSWORD: postgres volumes: - - ~/.mytb-edge-data/db:/var/lib/postgresql/data + - tb-edge-postgres-data:/var/lib/postgresql/data + +volumes: + tb-edge-data: + name: tb-edge-data + tb-edge-logs: + name: tb-edge-logs + tb-edge-postgres-data: + name: tb-edge-postgres-data {:copy-code} ``` diff --git a/application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md similarity index 97% rename from application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md rename to application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md index 425c08662b..6fcdcd50f2 100644 --- a/application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md @@ -1,4 +1,4 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the cloud. +Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. #### Install Java 11 (OpenJDK) ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11: @@ -49,7 +49,7 @@ echo "deb http://apt.postgresql.org/pub/repos/apt/ ${RELEASE}"-pgdg main | sudo # install and launch the postgresql service: sudo apt update -sudo apt -y install postgresql-12 +sudo apt -y install postgresql-15 sudo service postgresql start {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md new file mode 100644 index 0000000000..c0e721ace2 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md @@ -0,0 +1,15 @@ +#### Upgrading to ${TB_EDGE_VERSION}EDGE + +**ThingsBoard Edge package download:** +```bash +wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm +{:copy-code} +``` +##### ThingsBoard Edge service upgrade + +Install package: +```bash +sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm +{:copy-code} +``` +${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md new file mode 100644 index 0000000000..f1b9931e8b --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md @@ -0,0 +1,10 @@ +#### Upgrading to ${TB_EDGE_VERSION} + +Execute the following command to pull **${TB_EDGE_VERSION}** image: + +```bash +docker pull thingsboard/tb-edge:${TB_EDGE_VERSION} +{:copy-code} +``` + +${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md new file mode 100644 index 0000000000..a6c9c6731c --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md @@ -0,0 +1,23 @@ +Modify ‘main’ docker compose (`docker-compose.yml`) file for ThingsBoard Edge and update version of the image: +```bash +nano docker-compose.yml +{:copy-code} +``` + +```text +version: '3.8' +services: + mytbedge: + restart: always + image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" +... +``` + +Make sure your image is the set to **tb-edge-${TB_EDGE_VERSION}**. +Execute the following commands to up this docker compose directly: + +```bash +docker compose up -d +docker compose logs -f mytbedge +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md new file mode 100644 index 0000000000..d922fa9155 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md @@ -0,0 +1,61 @@ +Create docker compose file for ThingsBoard Edge upgrade process: + +```bash +> docker-compose-upgrade.yml && nano docker-compose-upgrade.yml +{:copy-code} +``` + +Add the following lines to the yml file: + +```bash +version: '3.8' +services: + mytbedge: + restart: on-failure + image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tb-edge + volumes: + - tb-edge-data:/data + - tb-edge-logs:/var/log/tb-edge + entrypoint: upgrade-tb-edge.sh + postgres: + restart: always + image: "postgres:15" + ports: + - "5432" + environment: + POSTGRES_DB: tb-edge + POSTGRES_PASSWORD: postgres + volumes: + - tb-edge-postgres-data:/var/lib/postgresql/data + +volumes: + tb-edge-data: + name: tb-edge-data + tb-edge-logs: + name: tb-edge-logs + tb-edge-postgres-data: + name: tb-edge-postgres-data +{:copy-code} +``` + +Execute the following command to start upgrade process: + +```bash +docker compose -f docker-compose-upgrade.yml up +{:copy-code} +``` + +Once upgrade process successfully completed, exit from the docker-compose shell by this combination: + +```text +Ctrl + C +``` + +Execute the following command to stop TB Edge upgrade container: + +```bash +docker compose -f docker-compose-upgrade.yml stop +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md new file mode 100644 index 0000000000..bb3ce268b8 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md @@ -0,0 +1,83 @@ +Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on Docker (Linux or MacOS). + +#### Prepare for upgrading ThingsBoard Edge +Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following command +to stop and remove currently running TB Edge container: + +```bash +docker compose stop +docker compose rm mytbedge +{:copy-code} +``` + +**OPTIONAL:** If you still rely on Docker Compose as docker-compose (with a hyphen) here is the list of the above commands: +```text +docker-compose stop +docker-compose rm mytbedge +``` + +##### Migrating Data from Docker Bind Mount Folders to Docker Volumes +Starting with the **3.6.2** release, the ThingsBoard team has transitioned from using Docker bind mount folders to Docker volumes. +This change aims to enhance security and efficiency in storing data for Docker containers and to mitigate permission issues across various environments. + +To migrate from Docker bind mounts to Docker volumes, please execute the following commands: + +```bash +docker run --rm -v tb-edge-data:/volume -v ~/.mytb-edge-data:/backup busybox sh -c "cp -a /backup/. /volume" +docker run --rm -v tb-edge-logs:/volume -v ~/.mytb-edge-logs:/backup busybox sh -c "cp -a /backup/. /volume" +docker run --rm -v tb-edge-postgres-data:/volume -v ~/.mytb-edge-data/db:/backup busybox sh -c "cp -a /backup/. /volume" +{:copy-code} +``` + +After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration. +Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings. + +First, please update docker compose file version. Find next snippet: +```text +version: '3.0' +... +``` + +And replace it with: +```text +version: '3.8' +... +``` + +Then update volume mounts. Locate the following snippet: +```text + volumes: + - ~/.mytb-edge-data:/data + - ~/.mytb-edge-logs:/var/log/tb-edge +... +``` + +And replace it with: +```text + volumes: + - tb-edge-data:/data + - tb-edge-logs:/var/log/tb-edge +... +``` + +Apply a similar update for the PostgreSQL service. Find the section: +```text + volumes: + - ~/.mytb-edge-data/db:/var/lib/postgresql/data +... +``` + +And replace it with: +```text + volumes: + - tb-edge-postgres-data/:/var/lib/postgresql/data +... +``` + +##### Backup Database +Make a copy of the database volume before upgrading: + +```bash +docker run --rm -v tb-edge-postgres-data:/source -v tb-edge-postgres-data-backup:/backup busybox sh -c "cp -a /source/. /backup" +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/start_service.md new file mode 100644 index 0000000000..c26c183154 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/start_service.md @@ -0,0 +1,6 @@ +Start the service + +```bash +sudo systemctl tb-edge start +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md new file mode 100644 index 0000000000..59d243c63c --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md @@ -0,0 +1,15 @@ +#### Upgrading to ${TB_EDGE_VERSION}EDGE + +**ThingsBoard Edge package download:** +```bash +wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb +{:copy-code} +``` +##### ThingsBoard Edge service upgrade + +Install package: +```bash +sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb +{:copy-code} +``` +${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md new file mode 100644 index 0000000000..6e7beb1425 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md @@ -0,0 +1,8 @@ +**NOTE**: Package installer may ask you to merge your tb-edge configuration. It is preferred to use **merge option** to make sure that all your previous parameters will not be overwritten. + +Execute regular upgrade script: + +```bash +sudo /usr/share/tb-edge/bin/install/upgrade.sh --fromVersion=${FROM_TB_EDGE_VERSION} +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md new file mode 100644 index 0000000000..bdd6c4ea18 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md @@ -0,0 +1,36 @@ +Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on ${OS} + +#### Prepare for upgrading ThingsBoard Edge + +Stop ThingsBoard Edge service: + +```bash +sudo systemctl stop tb-edge +{:copy-code} +``` + +##### Backup Database +Make a backup of the database before upgrading. **Make sure you have enough space to place a backup of the database.** + +Check database size: + +```bash +sudo -u postgres psql -c "SELECT pg_size_pretty( pg_database_size('tb_edge') );" +{:copy-code} +``` + +Check free space: + +```bash +df -h / +{:copy-code} +``` + +If there is enough free space - make a backup: + +```bash +sudo -Hiu postgres pg_dump tb_edge > tb_edge.sql.bak +{:copy-code} +``` + +Check backup file created successfully. diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index fa16fbff81..7556ab589b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; -import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; +import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -59,7 +59,8 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeBulkImportService; -import org.thingsboard.server.service.edge.instructions.EdgeInstallService; +import org.thingsboard.server.service.edge.instructions.EdgeInstallInstructionsService; +import org.thingsboard.server.service.edge.instructions.EdgeUpgradeInstructionsService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.edge.TbEdgeService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -101,7 +102,8 @@ public class EdgeController extends BaseController { private final EdgeBulkImportService edgeBulkImportService; private final TbEdgeService tbEdgeService; private final Optional edgeRpcServiceOpt; - private final Optional edgeInstallServiceOpt; + private final Optional edgeInstallServiceOpt; + private final Optional edgeUpgradeServiceOpt; public static final String EDGE_ID = "edgeId"; public static final String EDGE_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the edge is owned by the same tenant. " + @@ -553,23 +555,41 @@ public class EdgeController extends BaseController { return edgeBulkImportService.processBulkImport(request, user); } - @ApiOperation(value = "Get Edge Docker Install Instructions (getEdgeDockerInstallInstructions)", - notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, + @ApiOperation(value = "Get Edge Install Instructions (getEdgeInstallInstructions)", + notes = "Get an install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/instructions/{edgeId}/{method}", method = RequestMethod.GET) + @RequestMapping(value = "/edge/instructions/install/{edgeId}/{method}", method = RequestMethod.GET) @ResponseBody - public EdgeInstallInstructions getEdgeDockerInstallInstructions( + public EdgeInstructions getEdgeInstallInstructions( @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("edgeId") String strEdgeId, - @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')") + @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker, ubuntu, centos") @PathVariable("method") String installationMethod, HttpServletRequest request) throws ThingsboardException { if (isEdgesEnabled() && edgeInstallServiceOpt.isPresent()) { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); edgeId = checkNotNull(edgeId); Edge edge = checkEdgeId(edgeId, Operation.READ); - return checkNotNull(edgeInstallServiceOpt.get().getInstallInstructions(getTenantId(), edge, installationMethod, request)); + return checkNotNull(edgeInstallServiceOpt.get().getInstallInstructions(edge, installationMethod, request)); + } else { + throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); + } + } + + @ApiOperation(value = "Get Edge Upgrade Instructions (getEdgeUpgradeInstructions)", + notes = "Get an upgrade instructions for provided edge version." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/instructions/upgrade/{edgeVersion}/{method}", method = RequestMethod.GET) + @ResponseBody + public EdgeInstructions getEdgeUpgradeInstructions( + @ApiParam(value = "Edge version", required = true) + @PathVariable("edgeVersion") String edgeVersion, + @ApiParam(value = "Upgrade method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker, ubuntu, centos") + @PathVariable("method") String method) throws Exception { + if (isEdgesEnabled() && edgeUpgradeServiceOpt.isPresent()) { + return checkNotNull(edgeUpgradeServiceOpt.get().getUpgradeInstructions(edgeVersion, method)); } else { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java similarity index 78% rename from application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java rename to application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index dd17f83317..ba2cd06ee1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -16,13 +16,14 @@ package org.thingsboard.server.service.edge.instructions; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; -import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.edge.EdgeInstructions; +import org.thingsboard.server.dao.util.DeviceConnectivityUtil; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -37,11 +38,11 @@ import java.nio.file.Paths; @RequiredArgsConstructor @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @TbCoreComponent -public class DefaultEdgeInstallService implements EdgeInstallService { +public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstructionsService { private static final String EDGE_DIR = "edge"; - - private static final String EDGE_INSTALL_INSTRUCTIONS_DIR = "install_instructions"; + private static final String INSTRUCTIONS_DIR = "instructions"; + private static final String INSTALL_DIR = "install"; private final InstallScripts installScripts; @@ -52,10 +53,11 @@ public class DefaultEdgeInstallService implements EdgeInstallService { private boolean sslEnabled; @Value("${app.version:unknown}") + @Setter private String appVersion; @Override - public EdgeInstallInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request) { + public EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request) { switch (installationMethod.toLowerCase()) { case "docker": return getDockerInstallInstructions(edge, request); @@ -68,41 +70,41 @@ public class DefaultEdgeInstallService implements EdgeInstallService { } } - private EdgeInstallInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) { + private EdgeInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) { String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md")); String baseUrl = request.getServerName(); - if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) { - String localhostWarning = readFile(resolveFile("docker", "localhost_warning.md")); - dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", localhostWarning); - dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", "!!!REPLACE_ME_TO_HOST_IP_ADDRESS!!!"); + + if (DeviceConnectivityUtil.isLocalhost(baseUrl)) { + dockerInstallInstructions = dockerInstallInstructions.replace("${EXTRA_HOSTS}", "extra_hosts:\n - \"host.docker.internal:host-gateway\"\n"); + dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", "host.docker.internal"); } else { - dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", ""); + dockerInstallInstructions = dockerInstallInstructions.replace("${EXTRA_HOSTS}", ""); dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl); } String edgeVersion = appVersion + "EDGE"; edgeVersion = edgeVersion.replace("-SNAPSHOT", ""); dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge); - return new EdgeInstallInstructions(dockerInstallInstructions); + return new EdgeInstructions(dockerInstallInstructions); } - private EdgeInstallInstructions getUbuntuInstallInstructions(Edge edge, HttpServletRequest request) { + private EdgeInstructions getUbuntuInstallInstructions(Edge edge, HttpServletRequest request) { String ubuntuInstallInstructions = readFile(resolveFile("ubuntu", "instructions.md")); ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName()); String edgeVersion = appVersion.replace("-SNAPSHOT", ""); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - return new EdgeInstallInstructions(ubuntuInstallInstructions); + return new EdgeInstructions(ubuntuInstallInstructions); } - private EdgeInstallInstructions getCentosInstallInstructions(Edge edge, HttpServletRequest request) { + private EdgeInstructions getCentosInstallInstructions(Edge edge, HttpServletRequest request) { String centosInstallInstructions = readFile(resolveFile("centos", "instructions.md")); centosInstallInstructions = replacePlaceholders(centosInstallInstructions, edge); centosInstallInstructions = centosInstallInstructions.replace("${BASE_URL}", request.getServerName()); String edgeVersion = appVersion.replace("-SNAPSHOT", ""); centosInstallInstructions = centosInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - return new EdgeInstallInstructions(centosInstallInstructions); + return new EdgeInstructions(centosInstallInstructions); } private String replacePlaceholders(String instructions, Edge edge) { @@ -127,6 +129,6 @@ public class DefaultEdgeInstallService implements EdgeInstallService { } private Path getEdgeInstallInstructionsDir() { - return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, EDGE_INSTALL_INSTRUCTIONS_DIR); + return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, INSTALL_DIR); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java new file mode 100644 index 0000000000..940a813fc6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -0,0 +1,158 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.instructions; + +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EdgeUpgradeInfo; +import org.thingsboard.server.common.data.edge.EdgeInstructions; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.InstallScripts; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService { + + private static final Map upgradeVersionHashMap = new HashMap<>(); + + private static final String EDGE_DIR = "edge"; + private static final String INSTRUCTIONS_DIR = "instructions"; + private static final String UPGRADE_DIR = "upgrade"; + + private final InstallScripts installScripts; + + @Value("${app.version:unknown}") + @Setter + private String appVersion; + + @Override + public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) { + String tbVersion = appVersion.replace("-SNAPSHOT", ""); + String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); + switch (upgradeMethod.toLowerCase()) { + case "docker": + return getDockerUpgradeInstructions(tbVersion, currentEdgeVersion); + case "ubuntu": + case "centos": + return getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); + default: + throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); + } + } + + @Override + public void updateInstructionMap(Map map) { + for (String key : map.keySet()) { + upgradeVersionHashMap.put(key, map.get(key)); + } + } + + private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { + EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); + if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { + return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); + } + StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); + while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); + String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); + if (edgeUpgradeInfo.isRequiresUpdateDb()) { + String upgradeDb = readFile(resolveFile("docker", "upgrade_db.md")); + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); + } else { + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${UPGRADE_DB}", ""); + } + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); + currentEdgeVersion = edgeVersion; + edgeUpgradeInfo = upgradeVersionHashMap.get(edgeUpgradeInfo.getNextEdgeVersion()); + result.append(dockerUpgradeInstructions); + } + String startService = readFile(resolveFile("docker", "start_service.md")); + startService = startService.replace("${TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); + result.append(startService); + return new EdgeInstructions(result.toString()); + } + + private EdgeInstructions getLinuxUpgradeInstructions(String tbVersion, String currentEdgeVersion, String os) { + EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); + if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { + return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); + } + String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md")); + upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu"); + StringBuilder result = new StringBuilder(upgrade_preparing); + while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); + String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); + if (edgeUpgradeInfo.isRequiresUpdateDb()) { + String upgradeDb = readFile(resolveFile("upgrade_db.md")); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); + } else { + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${UPGRADE_DB}", ""); + } + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${FROM_TB_EDGE_TAG}", getTagVersion(currentEdgeVersion)); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion); + currentEdgeVersion = edgeVersion; + edgeUpgradeInfo = upgradeVersionHashMap.get(edgeUpgradeInfo.getNextEdgeVersion()); + result.append(linuxUpgradeInstructions); + } + String startService = readFile(resolveFile("start_service.md")); + result.append(startService); + return new EdgeInstructions(result.toString()); + } + + private String getTagVersion(String version) { + return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version; + } + + private String convertEdgeVersionToDocsFormat(String edgeVersion) { + return edgeVersion.replace("_", ".").substring(2); + } + + private String readFile(Path file) { + try { + return Files.readString(file); + } catch (IOException e) { + log.warn("Failed to read file: {}", file, e); + throw new RuntimeException(e); + } + } + + private Path resolveFile(String subDir, String... subDirs) { + return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs)); + } + + private Path getEdgeInstallInstructionsDir() { + return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, UPGRADE_DIR); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java similarity index 72% rename from application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java rename to application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java index 20cac25e33..9df1f86e86 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java @@ -16,12 +16,13 @@ package org.thingsboard.server.service.edge.instructions; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; -import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.edge.EdgeInstructions; import javax.servlet.http.HttpServletRequest; -public interface EdgeInstallService { +public interface EdgeInstallInstructionsService { - EdgeInstallInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request); + EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request); + + void setAppVersion(String version); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java new file mode 100644 index 0000000000..be40bc6c89 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.instructions; + +import org.thingsboard.server.common.data.EdgeUpgradeInfo; +import org.thingsboard.server.common.data.edge.EdgeInstructions; + +import java.util.Map; + +public interface EdgeUpgradeInstructionsService { + + EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod); + + void updateInstructionMap(Map upgradeVersions); + + void setAppVersion(String version); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index e1dc1c667f..c9fb796831 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -794,6 +795,7 @@ public final class EdgeGrpcSession implements Closeable { if (edge.getSecret().equals(request.getEdgeSecret())) { sessionOpenListener.accept(edge.getId(), this); this.edgeVersion = request.getEdgeVersion(); + processSaveEdgeVersionAsAttribute(request.getEdgeVersion().name()); return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.ACCEPTED) .setErrorMsg("") @@ -819,6 +821,11 @@ public final class EdgeGrpcSession implements Closeable { .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); } + private void processSaveEdgeVersionAsAttribute(String edgeVersion) { + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", edgeVersion), System.currentTimeMillis()); + ctx.getAttributesService().save(this.tenantId, this.edge.getId(), DataConstants.SERVER_SCOPE, attributeKvEntry); + } + @Override public void close() { log.debug("[{}][{}] Closing session", this.tenantId, sessionId); diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index f5e3daf05d..48a4187efc 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -27,17 +27,21 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.EdgeUpgradeMessage; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.instructions.EdgeInstallInstructionsService; +import org.thingsboard.server.service.edge.instructions.EdgeUpgradeInstructionsService; import javax.annotation.PreDestroy; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -65,12 +69,20 @@ public class DefaultUpdateService implements UpdateService { @Autowired private NotificationRuleProcessor notificationRuleProcessor; + @Autowired(required = false) + private EdgeInstallInstructionsService edgeInstallInstructionsService; + + @Autowired(required = false) + private EdgeUpgradeInstructionsService edgeUpgradeInstructionsService; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-update-service")); private ScheduledFuture checkUpdatesFuture = null; private final RestTemplate restClient = new RestTemplate(); private UpdateMessage updateMessage; + private EdgeUpgradeMessage edgeUpgradeMessage; + private String edgeInstallVersion; private String platform; private String version; @@ -82,6 +94,7 @@ public class DefaultUpdateService implements UpdateService { updateMessage = new UpdateMessage(false, version, "", "", "https://thingsboard.io/docs/reference/releases", "https://thingsboard.io/docs/reference/releases"); + edgeUpgradeMessage = new EdgeUpgradeMessage(new HashMap<>()); if (updatesEnabled) { try { platform = System.getProperty("platform", "unknown"); @@ -141,6 +154,16 @@ public class DefaultUpdateService implements UpdateService { .updateInfo(updateMessage) .build()); } + ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version); + String edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); + if (edgeInstallVersion != null) { + edgeInstallInstructionsService.setAppVersion(edgeInstallVersion); + edgeUpgradeInstructionsService.setAppVersion(edgeInstallVersion); + } + EdgeUpgradeMessage edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class); + if (edgeUpgradeMessage != null) { + edgeUpgradeInstructionsService.updateInstructionMap(edgeUpgradeMessage.getEdgeVersions()); + } } catch (Exception e) { log.trace(e.getMessage()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 28d48948d4..d4f4a4a241 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.EdgeUpgradeInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -74,6 +75,7 @@ import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; @@ -82,9 +84,11 @@ import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.instructions.EdgeUpgradeInstructionsService; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -115,6 +119,9 @@ public class EdgeControllerTest extends AbstractControllerTest { @Autowired private EdgeDao edgeDao; + @Autowired + private EdgeUpgradeInstructionsService edgeUpgradeInstructionsService; + static class Config { @Bean @Primary @@ -1172,8 +1179,25 @@ public class EdgeControllerTest extends AbstractControllerTest { public void testGetEdgeInstallInstructions() throws Exception { Edge edge = constructEdge(tenantId, "Edge for Test Docker Install Instructions", "default", "7390c3a6-69b0-9910-d155-b90aca4b772e", "l7q4zsjplzwhk16geqxy"); Edge savedEdge = doPost("/api/edge", edge, Edge.class); - String installInstructions = doGet("/api/edge/instructions/" + savedEdge.getId().getId().toString() + "/docker", String.class); + String installInstructions = doGet("/api/edge/instructions/install/" + savedEdge.getId().getId().toString() + "/docker", String.class); Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy")); Assert.assertTrue(installInstructions.contains("7390c3a6-69b0-9910-d155-b90aca4b772e")); } + + @Test + public void testGetEdgeUpgradeInstructions() throws Exception { + // UpdateInfo config is updating from Thingsboard Update server + HashMap upgradeInfoHashMap = new HashMap<>(); + upgradeInfoHashMap.put("3.6.0", new EdgeUpgradeInfo(true, "3.6.1")); + upgradeInfoHashMap.put("3.6.1", new EdgeUpgradeInfo(true, "3.6.2")); + upgradeInfoHashMap.put("3.6.2", new EdgeUpgradeInfo(true, null)); + edgeUpgradeInstructionsService.updateInstructionMap(upgradeInfoHashMap); + Edge edge = constructEdge("Edge for Test Docker Upgrade Instructions", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + String body = "{\"edgeVersion\": \"V_3_6_0\"}"; + doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); + String upgradeInstructions = doGet("/api/edge/instructions/upgrade/" + EdgeVersion.V_3_6_0.name() + "/docker", String.class); + Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.1EDGE")); + Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.2EDGE")); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java index 1d5a2c127f..faa29ad35d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java @@ -52,4 +52,4 @@ public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); } -} \ No newline at end of file +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java index f5d42046b7..abe1810887 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java @@ -78,4 +78,4 @@ public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSp processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk(); } -} \ No newline at end of file +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java new file mode 100644 index 0000000000..cadd589b49 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class EdgeUpgradeInfo { + private boolean requiresUpdateDb; + private String nextEdgeVersion; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java new file mode 100644 index 0000000000..bb17479607 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +@Data +@ApiModel +public class EdgeUpgradeMessage implements Serializable { + + private static final long serialVersionUID = 2872965507642822989L; + + @ApiModelProperty(position = 1, value = "Mapping for upgrade versions and upgrade strategy (next ver).") + private final Map edgeVersions; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstructions.java similarity index 90% rename from common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java rename to common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstructions.java index 8343058250..2c3a66c002 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstructions.java @@ -25,8 +25,8 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class EdgeInstallInstructions { +public class EdgeInstructions { - @ApiModelProperty(position = 1, value = "Markdown with install instructions") - private String installInstructions; + @ApiModelProperty(position = 1, value = "Markdown with install/upgrade instructions") + private String instructions; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index c5708ce34e..63ddba1c14 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -217,7 +217,7 @@ public class DeviceConnectivityUtil { return host; } - private static boolean isLocalhost(String host) { + public static boolean isLocalhost(String host) { try { InetAddress inetAddress = InetAddress.getByName(host); return inetAddress.isLoopbackAddress(); diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 8b17cef6c1..5fe2561f3f 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -84,7 +84,7 @@ import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeInfo; -import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; +import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.AlarmCommentId; @@ -3261,12 +3261,18 @@ public class RestClient implements Closeable { }).getBody(); } - public Optional getEdgeDockerInstallInstructions(EdgeId edgeId) { - ResponseEntity edgeInstallInstructionsResult = - restTemplate.getForEntity(baseURL + "/api/edge/instructions/{edgeId}", EdgeInstallInstructions.class, edgeId.getId()); + public Optional getEdgeInstallInstructions(EdgeId edgeId, String method) { + ResponseEntity edgeInstallInstructionsResult = + restTemplate.getForEntity(baseURL + "/api/edge/instructions/install/{edgeId}/{method}", EdgeInstructions.class, edgeId.getId(), method); return Optional.ofNullable(edgeInstallInstructionsResult.getBody()); } + public Optional getEdgeUpgradeInstructions(String edgeVersion, String method) { + ResponseEntity edgeUpgradeInstructionsResult = + restTemplate.getForEntity(baseURL + "/api/edge/instructions/upgrade/{edgeVersion}/{method}", EdgeInstructions.class, edgeVersion, method); + return Optional.ofNullable(edgeUpgradeInstructionsResult.getBody()); + } + public UUID saveEntitiesVersion(VersionCreateRequest request) { return restTemplate.postForEntity(baseURL + "/api/entities/vc/version", request, UUID.class).getBody(); } diff --git a/ui-ngx/src/app/core/http/edge.service.ts b/ui-ngx/src/app/core/http/edge.service.ts index 4ddcc69458..30fbb64c54 100644 --- a/ui-ngx/src/app/core/http/edge.service.ts +++ b/ui-ngx/src/app/core/http/edge.service.ts @@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { PageLink, TimePageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { EntitySubtype } from '@app/shared/models/entity-type.models'; -import { Edge, EdgeEvent, EdgeInfo, EdgeInstallInstructions, EdgeSearchQuery } from '@shared/models/edge.models'; +import { Edge, EdgeEvent, EdgeInfo, EdgeInstructions, EdgeSearchQuery } from '@shared/models/edge.models'; import { EntityId } from '@shared/models/id/entity-id'; import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; @@ -114,7 +114,11 @@ export class EdgeService { return this.http.post('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config)); } - public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable { - return this.http.get(`/api/edge/instructions/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config)); + public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable { + return this.http.get(`/api/edge/instructions/install/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeUpgradeInstructions(edgeVersion: string, method: string = 'ubuntu', config?: RequestConfig): Observable { + return this.http.get(`/api/edge/instructions/upgrade/${edgeVersion}/${method}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts index 99da1dfb14..6c9511d709 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts @@ -21,12 +21,21 @@ import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; -import { EdgeInfo, EdgeInstructionsMethod } from '@shared/models/edge.models'; +import { + EdgeInfo, + EdgeInstructions, + EdgeInstructionsMethod, + edgeVersionAttributeKey +} from '@shared/models/edge.models'; import { EdgeService } from '@core/http/edge.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { mergeMap, Observable } from 'rxjs'; export interface EdgeInstructionsDialogData { edge: EdgeInfo; afterAdd: boolean; + upgradeAvailable: boolean; } @Component({ @@ -49,12 +58,16 @@ export class EdgeInstructionsDialogComponent extends DialogComponent, + private attributeService: AttributeService, private edgeService: EdgeService) { super(store, router, dialogRef); if (this.data.afterAdd) { this.dialogTitle = 'edge.install-connect-instructions-edge-created'; this.showDontShowAgain = true; + } else if (this.data.upgradeAvailable) { + this.dialogTitle = 'edge.upgrade-instructions'; + this.showDontShowAgain = false; } else { this.dialogTitle = 'edge.install-connect-instructions'; this.showDontShowAgain = false; @@ -85,12 +98,22 @@ export class EdgeInstructionsDialogComponent extends DialogComponent { - this.contentData[method] = res.installInstructions; - this.loadedInstructions = true; - } - ); + let edgeInstructions$: Observable; + if (this.data.upgradeAvailable) { + edgeInstructions$ = this.attributeService.getEntityAttributes(this.data.edge.id, AttributeScope.SERVER_SCOPE, [edgeVersionAttributeKey]) + .pipe(mergeMap(attributes => { + if (attributes.length) { + const edgeVersion = attributes[0].value; + return this.edgeService.getEdgeUpgradeInstructions(edgeVersion, method); + } + })); + } else { + edgeInstructions$ = this.edgeService.getEdgeInstallInstructions(this.data.edge.id.id, method); + } + edgeInstructions$.subscribe(res => { + this.contentData[method] = res.instructions; + this.loadedInstructions = true; + }); } } } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 9c9a78f91a..3d0baad650 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -114,13 +114,26 @@
- +
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts index 78c8df6c08..b7cb2b1752 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts @@ -20,12 +20,15 @@ import { AppState } from '@core/core.state'; import { EntityComponent } from '@home/components/entity/entity.component'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; -import { EdgeInfo } from '@shared/models/edge.models'; +import { EdgeInfo, edgeVersionAttributeKey } from '@shared/models/edge.models'; import { TranslateService } from '@ngx-translate/core'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { generateSecret, guid } from '@core/utils'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { environment as env } from '@env/environment'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; @Component({ selector: 'tb-edge', @@ -37,9 +40,11 @@ export class EdgeComponent extends EntityComponent { entityType = EntityType; edgeScope: 'tenant' | 'customer' | 'customer_user'; + upgradeAvailable: boolean = false; constructor(protected store: Store, protected translate: TranslateService, + private attributeService: AttributeService, @Inject('entity') protected entityValue: EdgeInfo, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, public fb: UntypedFormBuilder, @@ -95,6 +100,7 @@ export class EdgeComponent extends EntityComponent { } }); this.generateRoutingKeyAndSecret(entity, this.entityForm); + this.checkEdgeVersion(); } updateFormState() { @@ -133,4 +139,25 @@ export class EdgeComponent extends EntityComponent { form.get('secret').patchValue(generateSecret(20), {emitEvent: false}); } } + + checkEdgeVersion() { + this.attributeService.getEntityAttributes(this.entity.id, AttributeScope.SERVER_SCOPE, [edgeVersionAttributeKey]) + .subscribe(attributes => { + if (attributes?.length) { + const edgeVersion = attributes[0].value; + const tbVersion = 'V_' + env.tbVersion.replaceAll('.', '_'); + this.upgradeAvailable = this.versionUpgradeSupported(edgeVersion) && (edgeVersion !== tbVersion); + } else { + this.upgradeAvailable = false; + } + } + ); + } + + private versionUpgradeSupported(edgeVersion: string): boolean { + const edgeVersionArray = edgeVersion.split('_'); + const major = parseInt(edgeVersionArray[1]); + const minor = parseInt(edgeVersionArray[2]); + return major >= 3 && minor >= 6; + } } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts index 6a80388b52..4429515705 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts @@ -558,7 +558,7 @@ export class EdgesTableConfigResolver implements Resolve { if (afterAdd) { @@ -610,9 +611,12 @@ export class EdgesTableConfigResolver implements Resolve { body: string; } -export interface EdgeInstallInstructions { - installInstructions: string; +export interface EdgeInstructions { + instructions: string; } export enum EdgeInstructionsMethod { @@ -188,3 +188,5 @@ export enum EdgeInstructionsMethod { centos, docker } + +export const edgeVersionAttributeKey = 'edgeVersion'; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 5e585783d5..0698f78913 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2021,6 +2021,7 @@ "sync-process-started-successfully": "Sync process started successfully!", "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

List of missing rule chain(s):
{{missingRuleChains}}", + "upgrade-instructions": "Upgrade Instructions", "widget-datasource-error": "This widget supports only EDGE entity datasource" }, "edge-event": {