[lxc-devel] [lxd/master] Fix 2.0 db migration
freeekanayaka on Github
lxc-bot at linuxcontainers.org
Fri Aug 16 09:53:38 UTC 2019
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190816/131d76b9/attachment.bin>
-------------- next part --------------
From c825df0c4371c5827a0921d68bca435c2d825437 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Fri, 16 Aug 2019 09:48:46 +0200
Subject: [PATCH 1/4] Use query.Transaction instead of manual tx management
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
lxd/db/cluster/open.go | 47 +++++++++++++++++++-----------------------
1 file changed, 21 insertions(+), 26 deletions(-)
diff --git a/lxd/db/cluster/open.go b/lxd/db/cluster/open.go
index 7d0aef3c25..e580e13de1 100644
--- a/lxd/db/cluster/open.go
+++ b/lxd/db/cluster/open.go
@@ -6,7 +6,7 @@ import (
"path/filepath"
"sync/atomic"
- "github.com/canonical/go-dqlite"
+ dqlite "github.com/canonical/go-dqlite"
"github.com/lxc/lxd/lxd/db/query"
"github.com/lxc/lxd/lxd/db/schema"
"github.com/lxc/lxd/lxd/util"
@@ -165,42 +165,37 @@ func EnsureSchema(db *sql.DB, address string, dir string) (bool, error) {
// 1. This is needed for referential integrity with other tables. Also,
// create a default profile.
if initial == 0 {
- tx, err := db.Begin()
- if err != nil {
- return false, err
- }
- stmt := `
+ err = query.Transaction(db, func(tx *sql.Tx) error {
+ stmt := `
INSERT INTO nodes(id, name, address, schema, api_extensions) VALUES(1, 'none', '0.0.0.0', ?, ?)
`
- _, err = tx.Exec(stmt, SchemaVersion, apiExtensions)
- if err != nil {
- tx.Rollback()
- return false, err
- }
+ _, err = tx.Exec(stmt, SchemaVersion, apiExtensions)
+ if err != nil {
+ return err
+ }
- // Default project
- stmt = `
+ // Default project
+ stmt = `
INSERT INTO projects (name, description) VALUES ('default', 'Default LXD project');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.images', 'true');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.profiles', 'true');
`
- _, err = tx.Exec(stmt)
- if err != nil {
- tx.Rollback()
- return false, err
- }
+ _, err = tx.Exec(stmt)
+ if err != nil {
+ return err
+ }
- // Default profile
- stmt = `
+ // Default profile
+ stmt = `
INSERT INTO profiles (name, description, project_id) VALUES ('default', 'Default LXD profile', 1)
`
- _, err = tx.Exec(stmt)
- if err != nil {
- tx.Rollback()
- return false, err
- }
+ _, err = tx.Exec(stmt)
+ if err != nil {
+ return err
+ }
- err = tx.Commit()
+ return nil
+ })
if err != nil {
return false, err
}
From 020f1c406900383a54e4f144d5d2bc7ffb2cae25 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Fri, 16 Aug 2019 10:12:59 +0200
Subject: [PATCH 2/4] Add copy of cluster schema version 14
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
lxd/db/migration.go | 406 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 406 insertions(+)
diff --git a/lxd/db/migration.go b/lxd/db/migration.go
index 2f16ec644b..9becccf8f1 100644
--- a/lxd/db/migration.go
+++ b/lxd/db/migration.go
@@ -296,3 +296,409 @@ var preClusteringTables = []string{
"storage_volumes",
"storage_volumes_config",
}
+
+// Copy of version 14 of the clustering schema. The data migration code from
+// LXD 2.0 is meant to be run against this schema. Further schema changes are
+// applied using the normal schema update logic.
+var clusterSchemaVersion14 = `
+CREATE TABLE certificates (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ fingerprint TEXT NOT NULL,
+ type INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ certificate TEXT NOT NULL,
+ UNIQUE (fingerprint)
+);
+CREATE TABLE config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ UNIQUE (key)
+);
+CREATE TABLE "containers" (
+ id INTEGER primary key AUTOINCREMENT NOT NULL,
+ node_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ architecture INTEGER NOT NULL,
+ type INTEGER NOT NULL,
+ ephemeral INTEGER NOT NULL DEFAULT 0,
+ creation_date DATETIME NOT NULL DEFAULT 0,
+ stateful INTEGER NOT NULL DEFAULT 0,
+ last_use_date DATETIME,
+ description TEXT,
+ project_id INTEGER NOT NULL,
+ expiry_date DATETIME,
+ UNIQUE (project_id, name),
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE,
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
+);
+CREATE TABLE containers_backups (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ container_id INTEGER NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ creation_date DATETIME,
+ expiry_date DATETIME,
+ container_only INTEGER NOT NULL default 0,
+ optimized_storage INTEGER NOT NULL default 0,
+ FOREIGN KEY (container_id) REFERENCES containers (id) ON DELETE CASCADE,
+ UNIQUE (container_id, name)
+);
+CREATE TABLE containers_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ container_id INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ FOREIGN KEY (container_id) REFERENCES containers (id) ON DELETE CASCADE,
+ UNIQUE (container_id, key)
+);
+CREATE VIEW containers_config_ref (project,
+ node,
+ name,
+ key,
+ value) AS
+ SELECT projects.name,
+ nodes.name,
+ containers.name,
+ containers_config.key,
+ containers_config.value
+ FROM containers_config
+ JOIN containers ON containers.id=containers_config.container_id
+ JOIN projects ON projects.id=containers.project_id
+ JOIN nodes ON nodes.id=containers.node_id;
+CREATE TABLE containers_devices (
+ id INTEGER primary key AUTOINCREMENT NOT NULL,
+ container_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ type INTEGER NOT NULL default 0,
+ FOREIGN KEY (container_id) REFERENCES containers (id) ON DELETE CASCADE,
+ UNIQUE (container_id, name)
+);
+CREATE TABLE containers_devices_config (
+ id INTEGER primary key AUTOINCREMENT NOT NULL,
+ container_device_id INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ FOREIGN KEY (container_device_id) REFERENCES containers_devices (id) ON DELETE CASCADE,
+ UNIQUE (container_device_id, key)
+);
+CREATE VIEW containers_devices_ref (project,
+ node,
+ name,
+ device,
+ type,
+ key,
+ value) AS
+ SELECT projects.name,
+ nodes.name,
+ containers.name,
+ containers_devices.name,
+ containers_devices.type,
+ coalesce(containers_devices_config.key,
+ ''),
+ coalesce(containers_devices_config.value,
+ '')
+ FROM containers_devices
+ LEFT OUTER JOIN containers_devices_config ON containers_devices_config.container_device_id=containers_devices.id
+ JOIN containers ON containers.id=containers_devices.container_id
+ JOIN projects ON projects.id=containers.project_id
+ JOIN nodes ON nodes.id=containers.node_id;
+CREATE INDEX containers_node_id_idx ON containers (node_id);
+CREATE TABLE containers_profiles (
+ id INTEGER primary key AUTOINCREMENT NOT NULL,
+ container_id INTEGER NOT NULL,
+ profile_id INTEGER NOT NULL,
+ apply_order INTEGER NOT NULL default 0,
+ UNIQUE (container_id, profile_id),
+ FOREIGN KEY (container_id) REFERENCES containers(id) ON DELETE CASCADE,
+ FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
+);
+CREATE VIEW containers_profiles_ref (project,
+ node,
+ name,
+ value) AS
+ SELECT projects.name,
+ nodes.name,
+ containers.name,
+ profiles.name
+ FROM containers_profiles
+ JOIN containers ON containers.id=containers_profiles.container_id
+ JOIN profiles ON profiles.id=containers_profiles.profile_id
+ JOIN projects ON projects.id=containers.project_id
+ JOIN nodes ON nodes.id=containers.node_id
+ ORDER BY containers_profiles.apply_order;
+CREATE INDEX containers_project_id_and_name_idx ON containers (project_id,
+ name);
+CREATE INDEX containers_project_id_and_node_id_and_name_idx ON containers (project_id,
+ node_id,
+ name);
+CREATE INDEX containers_project_id_and_node_id_idx ON containers (project_id,
+ node_id);
+CREATE INDEX containers_project_id_idx ON containers (project_id);
+CREATE TABLE "images" (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ fingerprint TEXT NOT NULL,
+ filename TEXT NOT NULL,
+ size INTEGER NOT NULL,
+ public INTEGER NOT NULL DEFAULT 0,
+ architecture INTEGER NOT NULL,
+ creation_date DATETIME,
+ expiry_date DATETIME,
+ upload_date DATETIME NOT NULL,
+ cached INTEGER NOT NULL DEFAULT 0,
+ last_use_date DATETIME,
+ auto_update INTEGER NOT NULL DEFAULT 0,
+ project_id INTEGER NOT NULL,
+ UNIQUE (project_id, fingerprint),
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
+);
+CREATE TABLE "images_aliases" (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ image_id INTEGER NOT NULL,
+ description TEXT,
+ project_id INTEGER NOT NULL,
+ UNIQUE (project_id, name),
+ FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE,
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
+);
+CREATE INDEX images_aliases_project_id_idx ON images_aliases (project_id);
+CREATE TABLE images_nodes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ image_id INTEGER NOT NULL,
+ node_id INTEGER NOT NULL,
+ UNIQUE (image_id, node_id),
+ FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE,
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE
+);
+CREATE INDEX images_project_id_idx ON images (project_id);
+CREATE TABLE images_properties (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ image_id INTEGER NOT NULL,
+ type INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE
+);
+CREATE TABLE images_source (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ image_id INTEGER NOT NULL,
+ server TEXT NOT NULL,
+ protocol INTEGER NOT NULL,
+ certificate TEXT NOT NULL,
+ alias TEXT NOT NULL,
+ FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE
+);
+CREATE TABLE networks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ state INTEGER NOT NULL DEFAULT 0,
+ UNIQUE (name)
+);
+CREATE TABLE networks_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ network_id INTEGER NOT NULL,
+ node_id INTEGER,
+ key TEXT NOT NULL,
+ value TEXT,
+ UNIQUE (network_id, node_id, key),
+ FOREIGN KEY (network_id) REFERENCES networks (id) ON DELETE CASCADE,
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE
+);
+CREATE TABLE networks_nodes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ network_id INTEGER NOT NULL,
+ node_id INTEGER NOT NULL,
+ UNIQUE (network_id, node_id),
+ FOREIGN KEY (network_id) REFERENCES networks (id) ON DELETE CASCADE,
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE
+);
+CREATE TABLE nodes (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ description TEXT DEFAULT '',
+ address TEXT NOT NULL,
+ schema INTEGER NOT NULL,
+ api_extensions INTEGER NOT NULL,
+ heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,
+ pending INTEGER NOT NULL DEFAULT 0,
+ UNIQUE (name),
+ UNIQUE (address)
+);
+CREATE TABLE "operations" (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ uuid TEXT NOT NULL,
+ node_id TEXT NOT NULL,
+ type INTEGER NOT NULL DEFAULT 0,
+ project_id INTEGER,
+ UNIQUE (uuid),
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE,
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
+);
+CREATE TABLE "profiles" (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ project_id INTEGER NOT NULL,
+ UNIQUE (project_id, name),
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
+);
+CREATE TABLE profiles_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ profile_id INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ UNIQUE (profile_id, key),
+ FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
+);
+CREATE VIEW profiles_config_ref (project,
+ name,
+ key,
+ value) AS
+ SELECT projects.name,
+ profiles.name,
+ profiles_config.key,
+ profiles_config.value
+ FROM profiles_config
+ JOIN profiles ON profiles.id=profiles_config.profile_id
+ JOIN projects ON projects.id=profiles.project_id;
+CREATE TABLE profiles_devices (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ profile_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ type INTEGER NOT NULL default 0,
+ UNIQUE (profile_id, name),
+ FOREIGN KEY (profile_id) REFERENCES profiles (id) ON DELETE CASCADE
+);
+CREATE TABLE profiles_devices_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ profile_device_id INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ UNIQUE (profile_device_id, key),
+ FOREIGN KEY (profile_device_id) REFERENCES profiles_devices (id) ON DELETE CASCADE
+);
+CREATE VIEW profiles_devices_ref (project,
+ name,
+ device,
+ type,
+ key,
+ value) AS
+ SELECT projects.name,
+ profiles.name,
+ profiles_devices.name,
+ profiles_devices.type,
+ coalesce(profiles_devices_config.key,
+ ''),
+ coalesce(profiles_devices_config.value,
+ '')
+ FROM profiles_devices
+ LEFT OUTER JOIN profiles_devices_config ON profiles_devices_config.profile_device_id=profiles_devices.id
+ JOIN profiles ON profiles.id=profiles_devices.profile_id
+ JOIN projects ON projects.id=profiles.project_id;
+CREATE INDEX profiles_project_id_idx ON profiles (project_id);
+CREATE VIEW profiles_used_by_ref (project,
+ name,
+ value) AS
+ SELECT projects.name,
+ profiles.name,
+ printf('/1.0/containers/%s?project=%s',
+ containers.name,
+ containers_projects.name)
+ FROM profiles
+ JOIN projects ON projects.id=profiles.project_id
+ JOIN containers_profiles
+ ON containers_profiles.profile_id=profiles.id
+ JOIN containers
+ ON containers.id=containers_profiles.container_id
+ JOIN projects AS containers_projects
+ ON containers_projects.id=containers.project_id;
+CREATE TABLE projects (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ UNIQUE (name)
+);
+CREATE TABLE projects_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ project_id INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
+ UNIQUE (project_id, key)
+);
+CREATE VIEW projects_config_ref (name,
+ key,
+ value) AS
+ SELECT projects.name,
+ projects_config.key,
+ projects_config.value
+ FROM projects_config
+ JOIN projects ON projects.id=projects_config.project_id;
+CREATE VIEW projects_used_by_ref (name,
+ value) AS
+ SELECT projects.name,
+ printf('/1.0/containers/%s?project=%s',
+ containers.name,
+ projects.name)
+ FROM containers JOIN projects ON project_id=projects.id UNION
+ SELECT projects.name,
+ printf('/1.0/images/%s',
+ images.fingerprint)
+ FROM images JOIN projects ON project_id=projects.id UNION
+ SELECT projects.name,
+ printf('/1.0/profiles/%s?project=%s',
+ profiles.name,
+ projects.name)
+ FROM profiles JOIN projects ON project_id=projects.id;
+CREATE TABLE storage_pools (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ driver TEXT NOT NULL,
+ description TEXT,
+ state INTEGER NOT NULL DEFAULT 0,
+ UNIQUE (name)
+);
+CREATE TABLE storage_pools_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ storage_pool_id INTEGER NOT NULL,
+ node_id INTEGER,
+ key TEXT NOT NULL,
+ value TEXT,
+ UNIQUE (storage_pool_id, node_id, key),
+ FOREIGN KEY (storage_pool_id) REFERENCES storage_pools (id) ON DELETE CASCADE,
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE
+);
+CREATE TABLE storage_pools_nodes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ storage_pool_id INTEGER NOT NULL,
+ node_id INTEGER NOT NULL,
+ UNIQUE (storage_pool_id, node_id),
+ FOREIGN KEY (storage_pool_id) REFERENCES storage_pools (id) ON DELETE CASCADE,
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE
+);
+CREATE TABLE "storage_volumes" (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ storage_pool_id INTEGER NOT NULL,
+ node_id INTEGER NOT NULL,
+ type INTEGER NOT NULL,
+ description TEXT,
+ snapshot INTEGER NOT NULL DEFAULT 0,
+ project_id INTEGER NOT NULL,
+ UNIQUE (storage_pool_id, node_id, project_id, name, type),
+ FOREIGN KEY (storage_pool_id) REFERENCES storage_pools (id) ON DELETE CASCADE,
+ FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE,
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
+);
+CREATE TABLE storage_volumes_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ storage_volume_id INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT,
+ UNIQUE (storage_volume_id, key),
+ FOREIGN KEY (storage_volume_id) REFERENCES storage_volumes (id) ON DELETE CASCADE
+);
+
+INSERT INTO schema (version, updated_at) VALUES (14, strftime("%s"))
+`
From 368a786154806785ea7a6f02446c43e8a01f11a4 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Fri, 16 Aug 2019 10:18:03 +0200
Subject: [PATCH 3/4] Add Dump parameter to db.OpenCluster()
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
lxd/api_cluster.go | 1 +
lxd/cluster/heartbeat_test.go | 3 ++-
lxd/cluster/membership_test.go | 8 +++++---
lxd/daemon.go | 2 +-
lxd/db/db.go | 4 +++-
lxd/db/testing.go | 2 +-
6 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/lxd/api_cluster.go b/lxd/api_cluster.go
index 21c133c40a..b79b18de65 100644
--- a/lxd/api_cluster.go
+++ b/lxd/api_cluster.go
@@ -654,6 +654,7 @@ func clusterPutDisable(d *Daemon) Response {
d.cluster, err = db.OpenCluster(
"db.bin", store, address, "/unused/db/dir",
d.config.DqliteSetupTimeout,
+ nil,
dqlite.WithDialFunc(d.gateway.DialFunc()),
dqlite.WithContext(d.gateway.Context()),
)
diff --git a/lxd/cluster/heartbeat_test.go b/lxd/cluster/heartbeat_test.go
index cbfc1b1640..f14ce62de9 100644
--- a/lxd/cluster/heartbeat_test.go
+++ b/lxd/cluster/heartbeat_test.go
@@ -254,7 +254,8 @@ func (f *heartbeatFixture) node() (*state.State, *cluster.Gateway, string) {
store := gateway.ServerStore()
dial := gateway.DialFunc()
state.Cluster, err = db.OpenCluster(
- "db.bin", store, address, "/unused/db/dir", 5*time.Second, dqlite.WithDialFunc(dial))
+ "db.bin", store, address, "/unused/db/dir", 5*time.Second, nil,
+ dqlite.WithDialFunc(dial))
require.NoError(f.t, err)
f.gateways[len(f.gateways)] = gateway
diff --git a/lxd/cluster/membership_test.go b/lxd/cluster/membership_test.go
index 6c3aff3f40..97e9c79e05 100644
--- a/lxd/cluster/membership_test.go
+++ b/lxd/cluster/membership_test.go
@@ -260,7 +260,7 @@ func TestJoin(t *testing.T) {
var err error
targetState.Cluster, err = db.OpenCluster(
"db.bin", targetStore, targetAddress, "/unused/db/dir",
- 10*time.Second,
+ 10*time.Second, nil,
dqlite.WithDialFunc(targetDialFunc))
require.NoError(t, err)
@@ -297,7 +297,8 @@ func TestJoin(t *testing.T) {
dialFunc := gateway.DialFunc()
state.Cluster, err = db.OpenCluster(
- "db.bin", store, address, "/unused/db/dir", 5*time.Second, dqlite.WithDialFunc(dialFunc))
+ "db.bin", store, address, "/unused/db/dir", 5*time.Second, nil,
+ dqlite.WithDialFunc(dialFunc))
require.NoError(t, err)
f := &membershipFixtures{t: t, state: state}
@@ -380,7 +381,8 @@ func FLAKY_TestPromote(t *testing.T) {
store := targetGateway.ServerStore()
dialFunc := targetGateway.DialFunc()
targetState.Cluster, err = db.OpenCluster(
- "db.bin", store, targetAddress, "/unused/db/dir", 5*time.Second, dqlite.WithDialFunc(dialFunc))
+ "db.bin", store, targetAddress, "/unused/db/dir", 5*time.Second, nil,
+ dqlite.WithDialFunc(dialFunc))
require.NoError(t, err)
targetF := &membershipFixtures{t: t, state: targetState}
targetF.ClusterAddress(targetAddress)
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 0aad1da9f9..58be2f8275 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -688,7 +688,7 @@ func (d *Daemon) init() error {
d.cluster, err = db.OpenCluster(
"db.bin", store, clusterAddress, dir,
- d.config.DqliteSetupTimeout,
+ d.config.DqliteSetupTimeout, dump,
dqlite.WithDialFunc(d.gateway.DialFunc()),
dqlite.WithContext(d.gateway.Context()),
dqlite.WithConnectionTimeout(10*time.Second),
diff --git a/lxd/db/db.go b/lxd/db/db.go
index b6633aafd6..b8137e4bb6 100644
--- a/lxd/db/db.go
+++ b/lxd/db/db.go
@@ -153,12 +153,14 @@ type Cluster struct {
// - dialer: Function used to connect to the dqlite backend via gRPC SQL.
// - address: Network address of this node (or empty string).
// - dir: Base LXD database directory (e.g. /var/lib/lxd/database)
+// - timeout: Give up trying to open the database after this amount of time.
+// - dump: If not nil, a copy of 2.0 db data, for migrating to 3.0.
//
// The address and api parameters will be used to determine if the cluster
// database matches our version, and possibly trigger a schema update. If the
// schema update can't be performed right now, because some nodes are still
// behind, an Upgrading error is returned.
-func OpenCluster(name string, store dqlite.ServerStore, address, dir string, timeout time.Duration, options ...dqlite.DriverOption) (*Cluster, error) {
+func OpenCluster(name string, store dqlite.ServerStore, address, dir string, timeout time.Duration, dump *Dump, options ...dqlite.DriverOption) (*Cluster, error) {
db, err := cluster.Open(name, store, options...)
if err != nil {
return nil, errors.Wrap(err, "failed to open database")
diff --git a/lxd/db/testing.go b/lxd/db/testing.go
index cee1366481..71755e22de 100644
--- a/lxd/db/testing.go
+++ b/lxd/db/testing.go
@@ -63,7 +63,7 @@ func NewTestCluster(t *testing.T) (*Cluster, func()) {
}
cluster, err := OpenCluster(
- "test.db", store, "1", "/unused/db/dir", 5*time.Second,
+ "test.db", store, "1", "/unused/db/dir", 5*time.Second, nil,
dqlite.WithLogFunc(log), dqlite.WithDialFunc(dial))
require.NoError(t, err)
From f007b1d1f8ce5afe181aa8256c2bf7523a4d7628 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Fri, 16 Aug 2019 10:38:20 +0200
Subject: [PATCH 4/4] Invoke data migration from db.OpenCluster, before schema
updates
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
lxd/daemon.go | 25 +-----------------------
lxd/db/db.go | 27 ++++++++++++++++++++++++++
lxd/db/migration.go | 41 +++++++++++++++++++++++++++-------------
lxd/db/migration_test.go | 15 +++++++++++++--
lxd/db/testing.go | 18 +++++++++++-------
5 files changed, 80 insertions(+), 46 deletions(-)
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 58be2f8275..bf7ae49484 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -21,7 +21,7 @@ import (
"github.com/gorilla/mux"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
- "gopkg.in/lxc/go-lxc.v2"
+ lxc "gopkg.in/lxc/go-lxc.v2"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
@@ -728,29 +728,6 @@ func (d *Daemon) init() error {
}
d.gateway.Cluster = d.cluster
- /* Migrate the node local data to the cluster database, if needed */
- if dump != nil {
- logger.Infof("Migrating data from local to global database")
- err = d.cluster.ImportPreClusteringData(dump)
- if err != nil {
- // Restore the local sqlite3 backup and wipe the raft
- // directory, so users can fix problems and retry.
- path := d.os.LocalDatabasePath()
- copyErr := shared.FileCopy(path+".bak", path)
- if copyErr != nil {
- // Ignore errors here, there's not much we can do
- logger.Errorf("Failed to restore local database: %v", copyErr)
- }
- rmErr := os.RemoveAll(d.os.GlobalDatabaseDir())
- if rmErr != nil {
- // Ignore errors here, there's not much we can do
- logger.Errorf("Failed to cleanup global database: %v", rmErr)
- }
-
- return fmt.Errorf("Failed to migrate data to global database: %v", err)
- }
- }
-
// This logic used to belong to patchUpdateFromV10, but has been moved
// here because it needs database access.
if shared.PathExists(shared.VarPath("lxc")) {
diff --git a/lxd/db/db.go b/lxd/db/db.go
index b8137e4bb6..341982d4d9 100644
--- a/lxd/db/db.go
+++ b/lxd/db/db.go
@@ -3,6 +3,8 @@ package db
import (
"database/sql"
"fmt"
+ "os"
+ "path/filepath"
"sync"
"time"
@@ -12,6 +14,7 @@ import (
"github.com/lxc/lxd/lxd/db/cluster"
"github.com/lxc/lxd/lxd/db/node"
"github.com/lxc/lxd/lxd/db/query"
+ "github.com/lxc/lxd/shared"
"github.com/lxc/lxd/shared/logger"
)
@@ -210,6 +213,30 @@ func OpenCluster(name string, store dqlite.ServerStore, address, dir string, tim
}
}
+ if dump != nil {
+ logger.Infof("Migrating data from local to global database")
+ err := query.Transaction(db, func(tx *sql.Tx) error {
+ return importPreClusteringData(tx, dump)
+ })
+ if err != nil {
+ // Restore the local sqlite3 backup and wipe the raft
+ // directory, so users can fix problems and retry.
+ path := filepath.Join(dir, "local.db")
+ copyErr := shared.FileCopy(path+".bak", path)
+ if copyErr != nil {
+ // Ignore errors here, there's not much we can do
+ logger.Errorf("Failed to restore local database: %v", copyErr)
+ }
+ rmErr := os.RemoveAll(filepath.Join(dir, "global"))
+ if rmErr != nil {
+ // Ignore errors here, there's not much we can do
+ logger.Errorf("Failed to cleanup global database: %v", rmErr)
+ }
+
+ return nil, errors.Wrap(err, "Failed to migrate data to global database")
+ }
+ }
+
nodesVersionsMatch, err := cluster.EnsureSchema(db, address, dir)
if err != nil {
return nil, errors.Wrap(err, "failed to ensure schema")
diff --git a/lxd/db/migration.go b/lxd/db/migration.go
index 9becccf8f1..c213d4db75 100644
--- a/lxd/db/migration.go
+++ b/lxd/db/migration.go
@@ -102,18 +102,31 @@ var preClusteringTablesRequiringProjectID = []string{
}
// ImportPreClusteringData imports the data loaded with LoadPreClusteringData.
-func (c *Cluster) ImportPreClusteringData(dump *Dump) error {
- tx, err := c.db.Begin()
+func importPreClusteringData(tx *sql.Tx, dump *Dump) error {
+ // Create version 14 of the cluster database schema.
+ _, err := tx.Exec(clusterSchemaVersion14)
if err != nil {
- return errors.Wrap(err, "failed to start cluster database transaction")
+ return errors.Wrap(err, "Create cluster database schema version 14")
}
- // Delete the default profile in the cluster database, which always
- // gets created no matter what.
- _, err = tx.Exec("DELETE FROM profiles WHERE id=1")
+ // Insert an entry for node 1.
+ stmt := `
+INSERT INTO nodes(id, name, address, schema, api_extensions) VALUES(1, 'none', '0.0.0.0', 14, 1)
+`
+ _, err = tx.Exec(stmt)
+ if err != nil {
+ return err
+ }
+
+ // Default project
+ stmt = `
+INSERT INTO projects (name, description) VALUES ('default', 'Default LXD project');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.images', 'true');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.profiles', 'true');
+`
+ _, err = tx.Exec(stmt)
if err != nil {
- tx.Rollback()
- return errors.Wrap(err, "failed to delete default profile")
+ return err
}
for _, table := range preClusteringTables {
@@ -216,16 +229,13 @@ func (c *Cluster) ImportPreClusteringData(dump *Dump) error {
stmt += fmt.Sprintf(" VALUES %s", query.Params(len(columns)))
result, err := tx.Exec(stmt, row...)
if err != nil {
- tx.Rollback()
return errors.Wrapf(err, "failed to insert row %d into %s", i, table)
}
n, err := result.RowsAffected()
if err != nil {
- tx.Rollback()
return errors.Wrapf(err, "no result count for row %d of %s", i, table)
}
if n != 1 {
- tx.Rollback()
return fmt.Errorf("could not insert %d int %s", i, table)
}
@@ -237,7 +247,7 @@ func (c *Cluster) ImportPreClusteringData(dump *Dump) error {
}
}
- return tx.Commit()
+ return nil
}
// Insert a row in one of the nodes association tables (storage_pools_nodes,
@@ -699,6 +709,11 @@ CREATE TABLE storage_volumes_config (
UNIQUE (storage_volume_id, key),
FOREIGN KEY (storage_volume_id) REFERENCES storage_volumes (id) ON DELETE CASCADE
);
-
+CREATE TABLE schema (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ version INTEGER NOT NULL,
+ updated_at DATETIME NOT NULL,
+ UNIQUE (version)
+);
INSERT INTO schema (version, updated_at) VALUES (14, strftime("%s"))
`
diff --git a/lxd/db/migration_test.go b/lxd/db/migration_test.go
index 316dd4f320..7e82e8b173 100644
--- a/lxd/db/migration_test.go
+++ b/lxd/db/migration_test.go
@@ -1,9 +1,13 @@
package db_test
import (
+ "context"
"database/sql"
+ "net"
"testing"
+ "time"
+ dqlite "github.com/canonical/go-dqlite"
"github.com/lxc/lxd/lxd/db"
"github.com/lxc/lxd/lxd/db/query"
"github.com/stretchr/testify/assert"
@@ -40,11 +44,18 @@ func TestImportPreClusteringData(t *testing.T) {
dump, err := db.LoadPreClusteringData(tx)
require.NoError(t, err)
- cluster, cleanup := db.NewTestCluster(t)
+ dir, store, cleanup := db.NewTestDqliteServer(t)
defer cleanup()
- err = cluster.ImportPreClusteringData(dump)
+ dial := func(ctx context.Context, address string) (net.Conn, error) {
+ return net.Dial("unix", address)
+ }
+
+ cluster, err := db.OpenCluster(
+ "test.db", store, "1", dir, 5*time.Second, dump,
+ dqlite.WithDialFunc(dial))
require.NoError(t, err)
+ defer cluster.Close()
// certificates
certs, err := cluster.CertificatesGet()
diff --git a/lxd/db/testing.go b/lxd/db/testing.go
index 71755e22de..b027ed2b33 100644
--- a/lxd/db/testing.go
+++ b/lxd/db/testing.go
@@ -6,6 +6,7 @@ import (
"io/ioutil"
"net"
"os"
+ "path/filepath"
"testing"
"time"
@@ -54,7 +55,7 @@ func NewTestNodeTx(t *testing.T) (*NodeTx, func()) {
// that can be used to clean it up when done.
func NewTestCluster(t *testing.T) (*Cluster, func()) {
// Create an in-memory dqlite SQL server and associated store.
- store, serverCleanup := newDqliteServer(t)
+ dir, store, serverCleanup := NewTestDqliteServer(t)
log := newLogFunc(t)
@@ -63,7 +64,7 @@ func NewTestCluster(t *testing.T) (*Cluster, func()) {
}
cluster, err := OpenCluster(
- "test.db", store, "1", "/unused/db/dir", 5*time.Second, nil,
+ "test.db", store, "1", dir, 5*time.Second, nil,
dqlite.WithLogFunc(log), dqlite.WithDialFunc(dial))
require.NoError(t, err)
@@ -95,10 +96,11 @@ func NewTestClusterTx(t *testing.T) (*ClusterTx, func()) {
return clusterTx, cleanup
}
-// Create a new in-memory dqlite server.
+// NewTestDqliteServer creates a new test dqlite server.
//
-// Return the newly created server store can be used to connect to it.
-func newDqliteServer(t *testing.T) (*dqlite.DatabaseServerStore, func()) {
+// Return the directory backing the test server and a newly created server
+// store that can be used to connect to it.
+func NewTestDqliteServer(t *testing.T) (string, *dqlite.DatabaseServerStore, func()) {
t.Helper()
listener, err := net.Listen("unix", "")
@@ -107,9 +109,11 @@ func newDqliteServer(t *testing.T) (*dqlite.DatabaseServerStore, func()) {
address := listener.Addr().String()
dir, dirCleanup := newDir(t)
+ err = os.Mkdir(filepath.Join(dir, "global"), 0755)
+ require.NoError(t, err)
info := dqlite.ServerInfo{ID: uint64(1), Address: listener.Addr().String()}
- server, err := dqlite.NewServer(info, dir)
+ server, err := dqlite.NewServer(info, filepath.Join(dir, "global"))
require.NoError(t, err)
err = server.Bootstrap([]dqlite.ServerInfo{info})
@@ -128,7 +132,7 @@ func newDqliteServer(t *testing.T) (*dqlite.DatabaseServerStore, func()) {
ctx := context.Background()
require.NoError(t, store.Set(ctx, []dqlite.ServerInfo{{Address: address}}))
- return store, cleanup
+ return dir, store, cleanup
}
var dqliteSerial = 0
More information about the lxc-devel
mailing list