lxd/db/cluster/open.go
func EnsureSchema(db *sql.DB, address string, dir string) (bool, error) {
	someNodesAreBehind := false
	apiExtensions := version.APIExtensionsCount()

	backupDone := false
	hook := (func(version int, tx *sql.Tx) error {
		// Check if this is a fresh instance.
		isUpdate, err := schema.DoesSchemaTableExist(tx)
		if err != nil {
			return errors.Wrap(err, "failed to check if schema table exists")
		}

		if !isUpdate {
			return nil
		}

		// Check if we're clustered
		clustered := true
		n, err := selectUnclusteredNodesCount(tx)
		if err != nil {
			return errors.Wrap(err, "failed to fetch unclustered nodes count")
		}
		if n > 1 {
			// This should never happen, since we only add nodes
			// with valid addresses, but check it for sanity.
			return fmt.Errorf("found more than one unclustered nodes")
		} else if n == 1 {
			clustered = false
		}

		// If we're not clustered, backup the local cluster database directory
		// before performing any schema change. This makes sense only in the
		// non-clustered case, because otherwise the directory would be
		// re-populated by replication.
		if !clustered && !backupDone {
			logger.Infof("Updating the LXD global schema. Backup made as \"global.bak\"")
			err := shared.DirCopy(
				filepath.Join(dir, "global"),
				filepath.Join(dir, "global.bak"),
			)
			if err != nil {
				return errors.Wrap(err, "failed to backup global database")
			}
			backupDone = true
		}

		if version == -1 {
			logger.Debugf("Running pre-update queries from file for global DB schema")
		} else {
			logger.Debugf("Updating global DB schema from %d to %d", version, version+1)
		}

		return nil
	})

	check := func(current int, tx *sql.Tx) error {
		// If we're bootstrapping a fresh schema, skip any check, since
		// it's safe to assume we are the only node.
		if current == 0 {
			return nil
		}

		// Check if we're clustered
		n, err := selectUnclusteredNodesCount(tx)
		if err != nil {
			return errors.Wrap(err, "failed to fetch unclustered nodes count")
		}
		if n > 1 {
			// This should never happen, since we only add nodes
			// with valid addresses, but check it for sanity.
			return fmt.Errorf("found more than one unclustered nodes")
		} else if n == 1 {
			address = "0.0.0.0" // We're not clustered
		}

		// Update the schema and api_extension columns of ourselves.
		err = updateNodeVersion(tx, address, apiExtensions)
		if err != nil {
			return errors.Wrap(err, "failed to update node version info")
		}

		err = checkClusterIsUpgradable(tx, [2]int{len(updates), apiExtensions})
		if err == errSomeNodesAreBehind {
			someNodesAreBehind = true
			return schema.ErrGracefulAbort
		}
		return err
	}

	schema := Schema()
	schema.File(filepath.Join(dir, "patch.global.sql")) // Optional custom queries
	schema.Check(check)
	schema.Hook(hook)

	var initial int
	err := query.Retry(func() error {
		var err error
		initial, err = schema.Ensure(db)
		return err
	})
	if someNodesAreBehind {
		return false, nil
	}
	if err != nil {
		return false, err
	}

	// When creating a database from scratch, insert an entry for node
	// 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 := `
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
		}

		// 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
		}

		// 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.Commit()
		if err != nil {
			return false, err
		}
	}

	return true, err
}
func EnsureSchema(db *sql.DB, address string, dir string) (bool, error) {
	someNodesAreBehind := false
	apiExtensions := version.APIExtensionsCount()

	backupDone := false
	hook := (func(version int, tx *sql.Tx) error {
		// Check if this is a fresh instance.
		isUpdate, err := schema.DoesSchemaTableExist(tx)
		if err != nil {
			return errors.Wrap(err, "failed to check if schema table exists")
		}

		if !isUpdate {
			return nil
		}

		// Check if we're clustered
		clustered := true
		n, err := selectUnclusteredNodesCount(tx)
		if err != nil {
			return errors.Wrap(err, "failed to fetch unclustered nodes count")
		}
		if n > 1 {
			// This should never happen, since we only add nodes
			// with valid addresses, but check it for sanity.
			return fmt.Errorf("found more than one unclustered nodes")
		} else if n == 1 {
			clustered = false
		}

		// If we're not clustered, backup the local cluster database directory
		// before performing any schema change. This makes sense only in the
		// non-clustered case, because otherwise the directory would be
		// re-populated by replication.
		if !clustered && !backupDone {
			logger.Infof("Updating the LXD global schema. Backup made as \"global.bak\"")
			err := shared.DirCopy(
				filepath.Join(dir, "global"),
				filepath.Join(dir, "global.bak"),
			)
			if err != nil {
				return errors.Wrap(err, "failed to backup global database")
			}
			backupDone = true
		}

		if version == -1 {
			logger.Debugf("Running pre-update queries from file for global DB schema")
		} else {
			logger.Debugf("Updating global DB schema from %d to %d", version, version+1)
		}

		return nil
	})

	check := func(current int, tx *sql.Tx) error {
		// If we're bootstrapping a fresh schema, skip any check, since
		// it's safe to assume we are the only node.
		if current == 0 {
			return nil
		}

		// Check if we're clustered
		n, err := selectUnclusteredNodesCount(tx)
		if err != nil {
			return errors.Wrap(err, "failed to fetch unclustered nodes count")
		}
		if n > 1 {
			// This should never happen, since we only add nodes
			// with valid addresses, but check it for sanity.
			return fmt.Errorf("found more than one unclustered nodes")
		} else if n == 1 {
			address = "0.0.0.0" // We're not clustered
		}

		// Update the schema and api_extension columns of ourselves.
		err = updateNodeVersion(tx, address, apiExtensions)
		if err != nil {
			return errors.Wrap(err, "failed to update node version info")
		}

		err = checkClusterIsUpgradable(tx, [2]int{len(updates), apiExtensions})
		if err == errSomeNodesAreBehind {
			someNodesAreBehind = true
			return schema.ErrGracefulAbort
		}
		return err
	}

	schema := Schema()
	schema.File(filepath.Join(dir, "patch.global.sql")) // Optional custom queries
	schema.Check(check)
	schema.Hook(hook)

	var initial int
	err := query.Retry(func() error {
		var err error
		initial, err = schema.Ensure(db)
		return err
	})
	if someNodesAreBehind {
		return false, nil
	}
	if err != nil {
		return false, err
	}

	// When creating a database from scratch, insert an entry for node
	// 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 := `
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
		}

		// 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
		}

		// 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.Commit()
		if err != nil {
			return false, err
		}
	}

	return true, err
}
// EnsureSchema applies all relevant schema updates to the cluster database.
//
// Before actually doing anything, this function will make sure that all nodes
// in the cluster have a schema version and a number of API extensions that
// match our one. If it's not the case, we either return an error (if some
// nodes have version greater than us and we need to be upgraded), or return
// false and no error (if some nodes have a lower version, and we need to wait
// till they get upgraded and restarted).