thingsboard-aplcache

Changes

application/pom.xml 39(+37 -2)

pom.xml 12(+12 -0)

Details

diff --git a/application/build.gradle b/application/build.gradle
index 25ad51d..1d8a526 100644
--- a/application/build.gradle
+++ b/application/build.gradle
@@ -57,6 +57,21 @@ ospackage {
         into "bin"
     }
 
+    // Copy the install files
+    from("target/bin/install/install.sh") {
+        fileMode 0775
+        into "bin/install"
+    }
+
+    from("target/bin/install/upgrade.sh") {
+        fileMode 0775
+        into "bin/install"
+    }
+
+    from("target/bin/install/logback.xml") {
+        into "bin/install"
+    }
+
     // Copy the config files
     from("target/conf") {
         exclude "${pkgName}.conf"

application/pom.xml 39(+37 -2)

diff --git a/application/pom.xml b/application/pom.xml
index 5c4146b..4241b66 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -27,7 +27,7 @@
     <artifactId>application</artifactId>
     <packaging>jar</packaging>
 
-    <name>Thingsboard Server Application</name>
+    <name>ThingsBoard Server Application</name>
     <url>https://thingsboard.io</url>
     <description>Open-source IoT Platform - Device management, data collection, processing and visualization
     </description>
@@ -138,6 +138,14 @@
             <artifactId>velocity-tools</artifactId>
         </dependency>
         <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-csv</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context-support</artifactId>
         </dependency>
@@ -368,6 +376,29 @@
                         </configuration>
                     </execution>
                     <execution>
+                        <id>copy-install</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/bin/install</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/scripts/install</directory>
+                                    <includes>
+                                        <include>**/*.sh</include>
+                                        <include>**/*.xml</include>
+                                    </includes>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                            <filters>
+                                <filter>src/main/filters/unix.properties</filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                    <execution>
                         <id>copy-windows-control</id>
                         <phase>process-resources</phase>
                         <goals>
@@ -387,7 +418,7 @@
                         </configuration>
                     </execution>
                     <execution>
-                        <id>copy-data-cql</id>
+                        <id>copy-data</id>
                         <phase>process-resources</phase>
                         <goals>
                             <goal>copy-resources</goal>
@@ -396,9 +427,13 @@
                             <outputDirectory>${project.build.directory}/data</outputDirectory>
                             <resources>
                                 <resource>
+                                    <directory>src/main/data</directory>
+                                </resource>
+                                <resource>
                                     <directory>../dao/src/main/resources</directory>
                                     <includes>
                                         <include>**/*.cql</include>
+                                        <include>**/*.sql</include>
                                     </includes>
                                     <filtering>false</filtering>
                                 </resource>
diff --git a/application/src/main/data/upgrade/1.3.0/schema_update.cql b/application/src/main/data/upgrade/1.3.0/schema_update.cql
new file mode 100644
index 0000000..4070614
--- /dev/null
+++ b/application/src/main/data/upgrade/1.3.0/schema_update.cql
@@ -0,0 +1,194 @@
+--
+-- Copyright © 2016-2017 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.
+--
+
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.device_by_tenant_and_name;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.device_by_tenant_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.device_by_tenant_by_type_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.device_by_customer_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.device_by_customer_by_type_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.device_types_by_tenant;
+
+DROP TABLE IF EXISTS thingsboard.device;
+
+CREATE TABLE IF NOT EXISTS thingsboard.device (
+    id timeuuid,
+    tenant_id timeuuid,
+    customer_id timeuuid,
+    name text,
+    type text,
+    search_text text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id, customer_id, type)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_name AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, name, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, type, search_text, id, customer_id)
+    WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, search_text, id, type )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, type, search_text, id )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_types_by_tenant AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( (type, tenant_id), id, customer_id)
+    WITH CLUSTERING ORDER BY ( id ASC, customer_id DESC);
+
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.asset_by_tenant_and_name;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.asset_by_tenant_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.asset_by_tenant_by_type_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.asset_by_customer_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.asset_by_customer_by_type_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.asset_types_by_tenant;
+
+DROP TABLE IF EXISTS thingsboard.asset;
+
+CREATE TABLE IF NOT EXISTS thingsboard.asset (
+    id timeuuid,
+    tenant_id timeuuid,
+    customer_id timeuuid,
+    name text,
+    type text,
+    search_text text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id, customer_id, type)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_name AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, name, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, type, search_text, id, customer_id)
+    WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, search_text, id, type )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, type, search_text, id )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_types_by_tenant AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( (type, tenant_id), id, customer_id)
+    WITH CLUSTERING ORDER BY ( id ASC, customer_id DESC);
+
+CREATE TABLE IF NOT EXISTS thingsboard.alarm (
+    id timeuuid,
+    tenant_id timeuuid,
+    type text,
+    originator_id timeuuid,
+    originator_type text,
+    severity text,
+    status text,
+    start_ts bigint,
+    end_ts bigint,
+    ack_ts bigint,
+    clear_ts bigint,
+    details text,
+    propagate boolean,
+    PRIMARY KEY ((tenant_id, originator_id, originator_type), type, id)
+) WITH CLUSTERING ORDER BY ( type ASC, id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.alarm_by_id AS
+    SELECT *
+    from thingsboard.alarm
+    WHERE tenant_id IS NOT NULL AND originator_id IS NOT NULL AND originator_type IS NOT NULL AND type IS NOT NULL
+    AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY (id, tenant_id, originator_id, originator_type, type)
+    WITH CLUSTERING ORDER BY ( tenant_id ASC, originator_id ASC, originator_type ASC, type ASC);
+
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.relation_by_type_and_child_type;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.reverse_relation;
+
+DROP TABLE IF EXISTS thingsboard.relation;
+
+CREATE TABLE IF NOT EXISTS thingsboard.relation (
+    from_id timeuuid,
+    from_type text,
+    to_id timeuuid,
+    to_type text,
+    relation_type_group text,
+    relation_type text,
+    additional_info text,
+    PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_id, to_type)
+) WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_id ASC, to_type ASC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.relation_by_type_and_child_type AS
+    SELECT *
+    from thingsboard.relation
+    WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
+    PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_type, to_id)
+    WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_type ASC, to_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.reverse_relation AS
+    SELECT *
+    from thingsboard.relation
+    WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
+    PRIMARY KEY ((to_id, to_type), relation_type_group, relation_type, from_id, from_type)
+    WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, from_id ASC, from_type ASC);
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index d14c3b5..61b3e8a 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -25,6 +25,7 @@ import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
 import org.thingsboard.server.service.install.DatabaseSchemaService;
+import org.thingsboard.server.service.install.DatabaseUpgradeService;
 import org.thingsboard.server.service.install.SystemDataLoaderService;
 
 import java.nio.file.Files;
@@ -35,6 +36,12 @@ import java.nio.file.Paths;
 @Slf4j
 public class ThingsboardInstallService {
 
+    @Value("${install.upgrade:false}")
+    private Boolean isUpgrade;
+
+    @Value("${install.upgrade.from_version:1.2.3}")
+    private String upgradeFromVersion;
+
     @Value("${install.data_dir}")
     private String dataDir;
 
@@ -45,6 +52,9 @@ public class ThingsboardInstallService {
     private DatabaseSchemaService databaseSchemaService;
 
     @Autowired
+    private DatabaseUpgradeService databaseUpgradeService;
+
+    @Autowired
     private ComponentDiscoveryService componentDiscoveryService;
 
     @Autowired
@@ -55,35 +65,67 @@ public class ThingsboardInstallService {
 
     public void performInstall() {
         try {
-            log.info("Starting ThingsBoard Installation...");
+            if (isUpgrade) {
+                log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion);
 
-            if (this.dataDir == null) {
-                throw new RuntimeException("'install.data_dir' property should specified!");
-            }
-            if (!Files.isDirectory(Paths.get(this.dataDir))) {
-                throw new RuntimeException("'install.data_dir' property value is not a valid directory!");
-            }
+                switch (upgradeFromVersion) {
+                    case "1.2.3":
+                        log.info("Upgrading ThingsBoard from version {} to 1.3.0 ...", upgradeFromVersion);
+
+                        databaseUpgradeService.upgradeDatabase(upgradeFromVersion);
+
+                        log.info("Updating system data...");
 
-            log.info("Installing DataBase schema...");
+                        systemDataLoaderService.deleteSystemWidgetBundle("charts");
+                        systemDataLoaderService.deleteSystemWidgetBundle("cards");
+                        systemDataLoaderService.deleteSystemWidgetBundle("maps");
+                        systemDataLoaderService.deleteSystemWidgetBundle("analogue_gauges");
+                        systemDataLoaderService.deleteSystemWidgetBundle("digital_gauges");
+                        systemDataLoaderService.deleteSystemWidgetBundle("gpio_widgets");
+                        systemDataLoaderService.deleteSystemWidgetBundle("alarm_widgets");
 
-            databaseSchemaService.createDatabaseSchema();
+                        systemDataLoaderService.loadSystemWidgets();
 
-            log.info("Loading system data...");
+                        break;
+                    default:
+                        throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
 
-            componentDiscoveryService.discoverComponents();
+                }
+                log.info("Upgrade finished successfully!");
 
-            systemDataLoaderService.createSysAdmin();
-            systemDataLoaderService.createAdminSettings();
-            systemDataLoaderService.loadSystemWidgets();
-            systemDataLoaderService.loadSystemPlugins();
-            systemDataLoaderService.loadSystemRules();
+            } else {
 
-            if (loadDemo) {
-                log.info("Loading demo data...");
-                systemDataLoaderService.loadDemoData();
+                log.info("Starting ThingsBoard Installation...");
+
+                if (this.dataDir == null) {
+                    throw new RuntimeException("'install.data_dir' property should specified!");
+                }
+                if (!Files.isDirectory(Paths.get(this.dataDir))) {
+                    throw new RuntimeException("'install.data_dir' property value is not a valid directory!");
+                }
+
+                log.info("Installing DataBase schema...");
+
+                databaseSchemaService.createDatabaseSchema();
+
+                log.info("Loading system data...");
+
+                componentDiscoveryService.discoverComponents();
+
+                systemDataLoaderService.createSysAdmin();
+                systemDataLoaderService.createAdminSettings();
+                systemDataLoaderService.loadSystemWidgets();
+                systemDataLoaderService.loadSystemPlugins();
+                systemDataLoaderService.loadSystemRules();
+
+                if (loadDemo) {
+                    log.info("Loading demo data...");
+                    systemDataLoaderService.loadDemoData();
+                }
+                log.info("Installation finished successfully!");
             }
 
-            log.info("Finished!");
+
         } catch (Exception e) {
             log.error("Unexpected error during ThingsBoard installation!", e);
             throw new ThingsboardInstallException("Unexpected error during ThingsBoard installation!", e);
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
index f146a27..eaef0e4 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
@@ -23,7 +23,7 @@ import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.dao.cassandra.CassandraInstallCluster;
 import org.thingsboard.server.dao.util.NoSqlDao;
-import org.thingsboard.server.install.cql.CQLStatementsParser;
+import org.thingsboard.server.service.install.cql.CQLStatementsParser;
 
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -35,6 +35,7 @@ import java.util.List;
 @Slf4j
 public class CassandraDatabaseSchemaService implements DatabaseSchemaService {
 
+    private static final String CASSANDRA_DIR = "cassandra";
     private static final String SCHEMA_CQL = "schema.cql";
 
     @Value("${install.data_dir}")
@@ -47,7 +48,7 @@ public class CassandraDatabaseSchemaService implements DatabaseSchemaService {
     public void createDatabaseSchema() throws Exception {
         log.info("Installing Cassandra DataBase schema...");
 
-        Path schemaFile = Paths.get(this.dataDir, SCHEMA_CQL);
+        Path schemaFile = Paths.get(this.dataDir, CASSANDRA_DIR, SCHEMA_CQL);
         loadCql(schemaFile);
 
     }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
new file mode 100644
index 0000000..4e769f7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright © 2016-2017 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.install;
+
+import com.datastax.driver.core.KeyspaceMetadata;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.dao.cassandra.CassandraCluster;
+import org.thingsboard.server.dao.cassandra.CassandraInstallCluster;
+import org.thingsboard.server.dao.util.NoSqlDao;
+import org.thingsboard.server.service.install.cql.CQLStatementsParser;
+import org.thingsboard.server.service.install.cql.CassandraDbHelper;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+@Service
+@NoSqlDao
+@Profile("install")
+@Slf4j
+public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
+
+    private static final String SCHEMA_UPDATE_CQL = "schema_update.cql";
+
+    @Value("${install.data_dir}")
+    private String dataDir;
+
+    @Autowired
+    private CassandraCluster cluster;
+
+    @Autowired
+    private CassandraInstallCluster installCluster;
+
+    @Override
+    public void upgradeDatabase(String fromVersion) throws Exception {
+
+        switch (fromVersion) {
+            case "1.2.3":
+
+                log.info("Upgrading Cassandara DataBase from version {} to 1.3.0 ...", fromVersion);
+
+                //Dump devices, assets and relations
+
+                KeyspaceMetadata ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName());
+
+                log.info("Dumping devices ...");
+                Path devicesDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), "device",
+                        new String[]{"id", "tenant_id", "customer_id", "name", "search_text", "additional_info"},
+                        "tb-devices");
+                if (devicesDump != null) {
+                    CassandraDbHelper.appendToEndOfLine(devicesDump, "default");
+                }
+                log.info("Devices dumped.");
+
+                log.info("Dumping assets ...");
+                Path assetsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), "asset",
+                        new String[]{"id", "tenant_id", "customer_id", "name", "search_text", "additional_info", "type"},
+                        "tb-assets");
+                log.info("Assets dumped.");
+
+                log.info("Dumping relations ...");
+                Path relationsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), "relation",
+                        new String[]{"from_id", "from_type", "to_id", "to_type", "relation_type", "additional_info"},
+                        "tb-relations");
+                if (relationsDump != null) {
+                    CassandraDbHelper.appendToEndOfLine(relationsDump, "COMMON");
+                }
+                log.info("Relations dumped.");
+
+                log.info("Updating schema ...");
+                Path schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.3.0", SCHEMA_UPDATE_CQL);
+                loadCql(schemaUpdateFile);
+                log.info("Schema updated.");
+
+                //Restore devices, assets and relations
+
+                log.info("Restoring devices ...");
+                if (devicesDump != null) {
+                    CassandraDbHelper.loadCf(ks, cluster.getSession(), "device",
+                            new String[]{"id", "tenant_id", "customer_id", "name", "search_text", "additional_info", "type"}, devicesDump);
+                    Files.deleteIfExists(devicesDump);
+                }
+                log.info("Devices restored.");
+
+                log.info("Restoring assets ...");
+                if (assetsDump != null) {
+                    CassandraDbHelper.loadCf(ks, cluster.getSession(), "asset",
+                            new String[]{"id", "tenant_id", "customer_id", "name", "search_text", "additional_info", "type"}, assetsDump);
+                    Files.deleteIfExists(assetsDump);
+                }
+                log.info("Assets restored.");
+
+                log.info("Restoring relations ...");
+                if (relationsDump != null) {
+                    CassandraDbHelper.loadCf(ks, cluster.getSession(), "relation",
+                            new String[]{"from_id", "from_type", "to_id", "to_type", "relation_type", "additional_info", "relation_type_group"}, relationsDump);
+                    Files.deleteIfExists(relationsDump);
+                }
+                log.info("Relations restored.");
+
+                break;
+            default:
+                throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
+        }
+
+    }
+
+    private void loadCql(Path cql) throws Exception {
+        List<String> statements = new CQLStatementsParser(cql).getStatements();
+        statements.forEach(statement -> installCluster.getSession().execute(statement));
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
new file mode 100644
index 0000000..4a92db2
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
@@ -0,0 +1,169 @@
+/**
+ * Copyright © 2016-2017 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.install.cql;
+
+import com.datastax.driver.core.*;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVPrinter;
+import org.apache.commons.csv.CSVRecord;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.*;
+
+public class CassandraDbHelper {
+
+    private static final CSVFormat CSV_DUMP_FORMAT = CSVFormat.DEFAULT.withNullString("\\N");
+
+    public static Path dumpCfIfExists(KeyspaceMetadata ks, Session session, String cfName,
+                                      String[] columns, String dumpPrefix) throws Exception {
+        if (ks.getTable(cfName) != null) {
+            Path dumpFile = Files.createTempFile(dumpPrefix, null);
+            Files.deleteIfExists(dumpFile);
+            try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(dumpFile), CSV_DUMP_FORMAT)) {
+                Statement stmt = new SimpleStatement("SELECT * FROM " + cfName);
+                stmt.setFetchSize(1000);
+                ResultSet rs = session.execute(stmt);
+                Iterator<Row> iter = rs.iterator();
+                while (iter.hasNext()) {
+                    Row row = iter.next();
+                    if (row != null) {
+                        dumpRow(row, columns, csvPrinter);
+                    }
+                }
+            }
+            return dumpFile;
+        } else {
+            return null;
+        }
+    }
+
+    public static void appendToEndOfLine(Path targetDumpFile, String toAppend) throws Exception {
+        Path tmp = Files.createTempFile(null, null);
+        try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(targetDumpFile), CSV_DUMP_FORMAT)) {
+            try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(tmp), CSV_DUMP_FORMAT)) {
+                csvParser.forEach(record -> {
+                    List<String> newRecord = new ArrayList<>();
+                    record.forEach(val -> newRecord.add(val));
+                    newRecord.add(toAppend);
+                    try {
+                        csvPrinter.printRecord(newRecord);
+                    } catch (IOException e) {
+                        throw new RuntimeException("Error appending to EOL", e);
+                    }
+                });
+            }
+        }
+        Files.move(tmp, targetDumpFile, StandardCopyOption.REPLACE_EXISTING);
+    }
+
+    public static void loadCf(KeyspaceMetadata ks, Session session, String cfName, String[] columns, Path sourceFile) throws Exception {
+        TableMetadata tableMetadata = ks.getTable(cfName);
+        PreparedStatement prepared = session.prepare(createInsertStatement(cfName, columns));
+        try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(sourceFile), CSV_DUMP_FORMAT.withHeader(columns))) {
+            csvParser.forEach(record -> {
+                BoundStatement boundStatement = prepared.bind();
+                for (String column : columns) {
+                    setColumnValue(tableMetadata, column, record, boundStatement);
+                }
+                session.execute(boundStatement);
+            });
+        }
+    }
+
+
+    private static void dumpRow(Row row, String[] columns, CSVPrinter csvPrinter) throws Exception {
+        List<String> record = new ArrayList<>();
+        for (String column : columns) {
+            record.add(getColumnValue(column, row));
+        }
+        csvPrinter.printRecord(record);
+    }
+
+    private static String getColumnValue(String column, Row row) {
+        String str = "";
+        int index = row.getColumnDefinitions().getIndexOf(column);
+        if (index > -1) {
+            DataType type = row.getColumnDefinitions().getType(index);
+            try {
+                if (row.isNull(index)) {
+                    return null;
+                } else if (type == DataType.cdouble()) {
+                    str = new Double(row.getDouble(index)).toString();
+                } else if (type == DataType.cint()) {
+                    str = new Integer(row.getInt(index)).toString();
+                } else if (type == DataType.uuid()) {
+                    str = row.getUUID(index).toString();
+                } else if (type == DataType.timeuuid()) {
+                    str = row.getUUID(index).toString();
+                } else if (type == DataType.cfloat()) {
+                    str = new Float(row.getFloat(index)).toString();
+                } else if (type == DataType.timestamp()) {
+                    str = ""+row.getTimestamp(index).getTime();
+                } else {
+                    str = row.getString(index);
+                }
+            } catch (Exception e) {
+                str = "";
+            }
+        }
+        return str;
+    }
+
+    private static String createInsertStatement(String cfName, String[] columns) {
+        StringBuilder insertStatementBuilder = new StringBuilder();
+        insertStatementBuilder.append("INSERT INTO ").append(cfName).append(" (");
+        for (String column : columns) {
+            insertStatementBuilder.append(column).append(",");
+        }
+        insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
+        insertStatementBuilder.append(") VALUES (");
+        for (String column : columns) {
+            insertStatementBuilder.append("?").append(",");
+        }
+        insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1);
+        insertStatementBuilder.append(")");
+        return insertStatementBuilder.toString();
+    }
+
+    private static void setColumnValue(TableMetadata tableMetadata, String column,
+                                       CSVRecord record, BoundStatement boundStatement) {
+        String value = record.get(column);
+        DataType type = tableMetadata.getColumn(column).getType();
+        if (value == null) {
+            boundStatement.setToNull(column);
+        } else if (type == DataType.cdouble()) {
+            boundStatement.setDouble(column, Double.valueOf(value));
+        } else if (type == DataType.cint()) {
+            boundStatement.setInt(column, Integer.valueOf(value));
+        } else if (type == DataType.uuid()) {
+            boundStatement.setUUID(column, UUID.fromString(value));
+        } else if (type == DataType.timeuuid()) {
+            boundStatement.setUUID(column, UUID.fromString(value));
+        } else if (type == DataType.cfloat()) {
+            boundStatement.setFloat(column, Float.valueOf(value));
+        } else if (type == DataType.timestamp()) {
+            boundStatement.setTimestamp(column, new Date(Long.valueOf(value)));
+        } else {
+            boundStatement.setString(column, value);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseUpgradeService.java
new file mode 100644
index 0000000..215e7bb
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseUpgradeService.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2017 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.install;
+
+public interface DatabaseUpgradeService {
+
+    void upgradeDatabase(String fromVersion) throws Exception;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index ff55274..1da39ab 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -41,6 +41,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceCredentialsService;
 import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
@@ -227,6 +228,14 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
         loadDashboards(Paths.get(dataDir, JSON_DIR, DEMO_DIR, DASHBOARDS_DIR), demoTenant.getId(), null);
     }
 
+    @Override
+    public void deleteSystemWidgetBundle(String bundleAlias) throws Exception {
+        WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(new TenantId(ModelConstants.NULL_UUID), bundleAlias);
+        if (widgetsBundle != null) {
+            widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getId());
+        }
+    }
+
     private User createUser(Authority authority,
                             TenantId tenantId,
                             CustomerId customerId,
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
index fc92de8..9d22776 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
@@ -22,20 +22,44 @@ import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.dao.util.SqlDao;
 
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.Connection;
+import java.sql.DriverManager;
+
 @Service
 @Profile("install")
 @Slf4j
 @SqlDao
 public class SqlDatabaseSchemaService implements DatabaseSchemaService {
 
+    private static final String SQL_DIR = "sql";
+    private static final String SCHEMA_SQL = "schema.sql";
+
     @Value("${install.data_dir}")
     private String dataDir;
 
+    @Value("${spring.datasource.url}")
+    private String dbUrl;
+
+    @Value("${spring.datasource.username}")
+    private String dbUserName;
+
+    @Value("${spring.datasource.password}")
+    private String dbPassword;
+
     @Override
     public void createDatabaseSchema() throws Exception {
 
         log.info("Installing SQL DataBase schema...");
-        //TODO:
+
+        Path schemaFile = Paths.get(this.dataDir, SQL_DIR, SCHEMA_SQL);
+        try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+            String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8"));
+            conn.createStatement().execute(sql);
+        }
 
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
new file mode 100644
index 0000000..fed8388
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2017 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.install;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.dao.util.SqlDao;
+
+@Service
+@Profile("install")
+@Slf4j
+@SqlDao
+public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
+
+    @Override
+    public void upgradeDatabase(String fromVersion) throws Exception {
+        switch (fromVersion) {
+            default:
+                throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
index 1a07428..8ca6f14 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
@@ -29,4 +29,6 @@ public interface SystemDataLoaderService {
 
     void loadDemoData() throws Exception;
 
+    void deleteSystemWidgetBundle(String bundleAlias) throws Exception;
+
 }
diff --git a/application/src/main/scripts/install/install.sh b/application/src/main/scripts/install/install.sh
new file mode 100755
index 0000000..be96363
--- /dev/null
+++ b/application/src/main/scripts/install/install.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+#
+# Copyright © 2016-2017 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.
+#
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+
+case $key in
+    --loadDemo)
+    LOAD_DEMO=true
+    shift # past argument
+    ;;
+    *)
+            # unknown option
+    ;;
+esac
+shift # past argument or value
+done
+
+if [ "$LOAD_DEMO" == "true" ]; then
+    loadDemo=true
+else
+    loadDemo=false
+fi
+
+CONF_FOLDER=${pkg.installFolder}/conf
+configfile=${pkg.name}.conf
+jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
+installDir=${pkg.installFolder}/data
+
+source "${CONF_FOLDER}/${configfile}"
+
+run_user=${pkg.name}
+
+su -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
+                    -Dinstall.data_dir=${installDir} \
+                    -Dinstall.load_demo=${loadDemo} \
+                    -Dspring.jpa.hibernate.ddl-auto=none \
+                    -Dinstall.upgrade=false \
+                    -Dlogging.config=${pkg.installFolder}/bin/install/logback.xml \
+                    org.springframework.boot.loader.PropertiesLauncher" "$run_user"
+
+if [ $? -ne 0 ]; then
+    echo "ThingsBoard installation failed!"
+else
+    echo "ThingsBoard installed successfully!"
+fi
+
+exit $?
diff --git a/application/src/main/scripts/install/logback.xml b/application/src/main/scripts/install/logback.xml
new file mode 100644
index 0000000..aadd92f
--- /dev/null
+++ b/application/src/main/scripts/install/logback.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+    Copyright © 2016-2017 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.
+
+-->
+<!DOCTYPE configuration>
+<configuration>
+
+    <appender name="fileLogAppender"
+              class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${pkg.logFolder}/install.log</file>
+        <rollingPolicy
+                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${pkg.logFolder}/install.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <maxFileSize>100MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>3GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.thingsboard.server.install" level="INFO">
+        <appender-ref ref="STDOUT" />
+    </logger>
+
+    <logger name="org.thingsboard.server.service.install" level="INFO">
+        <appender-ref ref="STDOUT" />
+    </logger>
+
+    <logger name="org.thingsboard.server" level="INFO" />
+    <logger name="akka" level="INFO" />
+
+    <root level="INFO">
+        <appender-ref ref="fileLogAppender"/>
+    </root>
+
+</configuration>
diff --git a/application/src/main/scripts/install/upgrade.sh b/application/src/main/scripts/install/upgrade.sh
new file mode 100755
index 0000000..b40c3bb
--- /dev/null
+++ b/application/src/main/scripts/install/upgrade.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+#
+# Copyright © 2016-2017 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.
+#
+
+for i in "$@"
+do
+case $i in
+    --fromVersion=*)
+    FROM_VERSION="${i#*=}"
+    shift
+    ;;
+    *)
+            # unknown option
+    ;;
+esac
+done
+
+if [[ -z "${FROM_VERSION// }" ]]; then
+    echo "--fromVersion parameter is invalid or unspecified!"
+    echo "Usage: upgrade.sh --fromVersion={VERSION}"
+    exit 1
+else
+    fromVersion="${FROM_VERSION// }"
+fi
+
+CONF_FOLDER=${pkg.installFolder}/conf
+configfile=${pkg.name}.conf
+jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
+installDir=${pkg.installFolder}/data
+
+source "${CONF_FOLDER}/${configfile}"
+
+run_user=${pkg.name}
+
+su -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
+                    -Dinstall.data_dir=${installDir} \
+                    -Dspring.jpa.hibernate.ddl-auto=none \
+                    -Dinstall.upgrade=true \
+                    -Dinstall.upgrade.from_version=${fromVersion} \
+                    -Dlogging.config=${pkg.installFolder}/bin/install/logback.xml \
+                    org.springframework.boot.loader.PropertiesLauncher" "$run_user"
+
+if [ $? -ne 0 ]; then
+    echo "ThingsBoard upgrade failed!"
+else
+    echo "ThingsBoard upgraded successfully!"
+fi
+
+exit $?
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
index ea4f9d1..2b9000d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
@@ -127,6 +127,10 @@ public abstract class AbstractCassandraCluster {
         }
     }
 
+    public String getKeyspaceName() {
+        return keyspaceName;
+    }
+
     private boolean isInstall() {
         return environment.acceptsProfiles("install");
     }

pom.xml 12(+12 -0)

diff --git a/pom.xml b/pom.xml
index 762d0dc..171dc60 100755
--- a/pom.xml
+++ b/pom.xml
@@ -46,6 +46,8 @@
         <guava.version>18.0</guava.version>
         <commons-lang3.version>3.4</commons-lang3.version>
         <commons-validator.version>1.5.0</commons-validator.version>
+        <commons-io.version>2.5</commons-io.version>
+        <commons-csv.version>1.4</commons-csv.version>
         <jackson.version>2.8.8.1</jackson.version>
         <json-schema-validator.version>2.2.6</json-schema-validator.version>
         <scala.version>2.11</scala.version>
@@ -566,6 +568,16 @@
                 <version>${commons-validator.version}</version>
             </dependency>
             <dependency>
+                <groupId>commons-io</groupId>
+                <artifactId>commons-io</artifactId>
+                <version>${commons-io.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-csv</artifactId>
+                <version>${commons-csv.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>com.fasterxml.jackson.core</groupId>
                 <artifactId>jackson-databind</artifactId>
                 <version>${jackson.version}</version>
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index 1388536..dde6228 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -892,10 +892,10 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
         }
     }
 
-    function getRelatedEntities(rootEntityId, entityType, entitySubTypes, maxLevel, keys, typeTranslatePrefix, relationType) {
+    function getRelatedEntities(rootEntityId, entityType, entitySubTypes, maxLevel, keys, typeTranslatePrefix, relationType, direction) {
         var deferred = $q.defer();
 
-        var entitySearchQuery = constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel, relationType);
+        var entitySearchQuery = constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel, relationType, direction);
         if (!entitySearchQuery) {
             deferred.reject();
         } else {
@@ -930,12 +930,15 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
         return deferred.promise;
     }
 
-    function saveRelatedEntity(relatedEntity, parentEntityId, keys, relation) {
+    function saveRelatedEntity(relatedEntity, parentEntityId, keys, relation, direction) {
         var deferred = $q.defer();
+        if (!direction) {
+            direction = types.entitySearchDirection.from;
+        }
         if (relatedEntity.id.id) {
-            updateRelatedEntity(relatedEntity, keys, deferred, relation);
+            updateRelatedEntity(relatedEntity, keys, deferred, relation, direction);
         } else {
-            addRelatedEntity(relatedEntity, parentEntityId, keys, deferred, relation);
+            addRelatedEntity(relatedEntity, parentEntityId, keys, deferred, relation, direction);
         }
         return deferred.promise;
     }
@@ -1073,7 +1076,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
         }
     }
 
-    function addRelatedEntity(relatedEntity, parentEntityId, keys, deferred, relation) {
+    function addRelatedEntity(relatedEntity, parentEntityId, keys, deferred, relation, direction) {
         var entity = {};
         entity.id = relatedEntity.id;
         entity.name = relatedEntity.name;
@@ -1083,13 +1086,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
                 relatedEntity.id = entity.id;
                 if (!relation) {
                     relation = {
-                        from: parentEntityId,
-                        to: relatedEntity.id,
                         type: types.entityRelationType.contains
                     };
-                } else {
+                }
+
+                if (direction == types.entitySearchDirection.from) {
+                    relation.from = parentEntityId;
                     relation.to = relatedEntity.id;
+                } else {
+                    relation.from = relatedEntity.id;
+                    relation.to = parentEntityId;
                 }
+
                 entityRelationService.saveRelation(relation).then(
                     function success() {
                         updateEntity(entity, relatedEntity, keys, deferred, relation);
@@ -1105,11 +1113,15 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
         );
     }
 
-    function updateRelatedEntity(relatedEntity, keys, deferred, relation) {
+    function updateRelatedEntity(relatedEntity, keys, deferred, relation, direction) {
         getEntityPromise(relatedEntity.id.entityType, relatedEntity.id.id, {ignoreLoading: true}).then(
             function success(entity) {
                 if (relation) {
-                    relation.to = relatedEntity.id;
+                    if (direction == types.entitySearchDirection.from) {
+                        relation.to = relatedEntity.id;
+                    } else {
+                        relation.from = relatedEntity.id;
+                    }
                     entityRelationService.saveRelation(relation).then(
                         function success() {
                             updateEntity(entity, relatedEntity, keys, deferred);
@@ -1162,16 +1174,19 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
             );
     }
 
-    function constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel, relationType) {
+    function constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel, relationType, direction) {
 
         var searchQuery = {
             parameters: {
                 rootId: rootEntityId.id,
                 rootType: rootEntityId.entityType,
-                direction: types.entitySearchDirection.from
+                direction: direction
             },
             relationType: relationType
         };
+        if (!direction) {
+            searchQuery.parameters.direction = types.entitySearchDirection.from;
+        }
         if (!relationType) {
             searchQuery.relationType = types.entityRelationType.contains;
         }
diff --git a/ui/src/app/components/related-entity-autocomplete.directive.js b/ui/src/app/components/related-entity-autocomplete.directive.js
index f93ded2..7ce0252 100644
--- a/ui/src/app/components/related-entity-autocomplete.directive.js
+++ b/ui/src/app/components/related-entity-autocomplete.directive.js
@@ -44,16 +44,14 @@ function RelatedEntityAutocomplete($compile, $templateCache, $q, $filter, entity
             if (!scope.allEntities) {
                 entityService.getRelatedEntities(scope.rootEntityId, scope.entityType, scope.entitySubtypes, -1, []).then(
                     function success(entities) {
-                        if (scope.excludeEntityId) {
-                            var result = $filter('filter')(entities, {id: {id: scope.excludeEntityId.id} }, true);
-                            result = $filter('filter')(result, {id: {entityType: scope.excludeEntityId.entityType} }, true);
-                            if (result && result.length) {
-                                var excludeEntity = result[0];
-                                var index = entities.indexOf(excludeEntity);
-                                if (index > -1) {
-                                    entities.splice(index, 1);
+                        if (scope.excludeEntityIds && scope.excludeEntityIds.length) {
+                            var filteredEntities = [];
+                            entities.forEach(function(entity) {
+                                if (scope.excludeEntityIds.indexOf(entity.id.id) == -1) {
+                                    filteredEntities.push(entity);
                                 }
-                            }
+                            });
+                            entities = filteredEntities;
                         }
                         scope.allEntities = entities;
                         filterEntities(searchText, deferred);
@@ -116,7 +114,7 @@ function RelatedEntityAutocomplete($compile, $templateCache, $q, $filter, entity
             rootEntityId: '=',
             entityType: '=',
             entitySubtypes: '=',
-            excludeEntityId: '=?',
+            excludeEntityIds: '=?',
             theForm: '=?',
             tbRequired: '=?',
             disabled:'=ngDisabled',