lxd/db/migration.go
func (c *Cluster) ImportPreClusteringData(dump *Dump) error {
	tx, err := c.db.Begin()
	if err != nil {
		return errors.Wrap(err, "failed to start cluster database transaction")
	}

	// Delete the default profile in the cluster database, which always
	// gets created no matter what.
	_, err = tx.Exec("DELETE FROM profiles WHERE id=1")
	if err != nil {
		tx.Rollback()
		return errors.Wrap(err, "failed to delete default profile")
	}

	for _, table := range preClusteringTables {
		logger.Debugf("Migrating data for table %s", table)

		for i, row := range dump.Data[table] {
			for i, element := range row {
				// Convert []byte columns to string. This is safe to do since
				// the pre-clustering schema only had TEXT fields and no BLOB.
				bytes, ok := element.([]byte)
				if ok {
					row[i] = string(bytes)
				}
			}
			columns := dump.Schema[table]

			nullNodeID := false // Whether node-related rows should have a NULL node ID
			appendNodeID := func() {
				columns = append(columns, "node_id")
				if nullNodeID {
					row = append(row, nil)
				} else {
					row = append(row, int64(1))
				}
			}

			switch table {
			case "config":
				// Don't migrate the core.https_address and maas.machine config keys,
				// which is node-specific and must remain in the node
				// database.
				keys := []string{"core.https_address", "maas.machine"}
				skip := false
				for i, column := range columns {
					value, ok := row[i].(string)
					if !ok {
						continue
					}
					if column == "key" && shared.StringInSlice(value, keys) {
						skip = true
					}
				}
				if skip {
					continue
				}
			case "containers":
				appendNodeID()
			case "networks_config":
				// The keys listed in NetworkNodeConfigKeys
				// are the only ones which are not global to the
				// cluster, so all other keys will have a NULL
				// node_id.
				index := 0
				for i, column := range columns {
					if column == "key" {
						index = i
						break
					}
				}
				key := row[index].(string)
				if !shared.StringInSlice(key, NetworkNodeConfigKeys) {
					nullNodeID = true
					break
				}
				appendNodeID()
			case "storage_pools_config":
				// The keys listed in StoragePoolNodeConfigKeys
				// are the only ones which are not global to the
				// cluster, so all other keys will have a NULL
				// node_id.
				index := 0
				for i, column := range columns {
					if column == "key" {
						index = i
						break
					}
				}
				key := row[index].(string)
				if !shared.StringInSlice(key, StoragePoolNodeConfigKeys) {
					nullNodeID = true
					break
				}
				appendNodeID()
			case "networks":
				fallthrough
			case "storage_pools":
				columns = append(columns, "state")
				row = append(row, storagePoolCreated)
			case "storage_volumes":
				appendNodeID()
			}

			if shared.StringInSlice(table, preClusteringTablesRequiringProjectID) {
				// These tables have a project_id reference in the new schema.
				columns = append(columns, "project_id")
				row = append(row, 1) // Reference the default project.
			}

			stmt := fmt.Sprintf("INSERT INTO %s(%s)", table, strings.Join(columns, ", "))
			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)
			}

			// Also insert the image ID to node ID association.
			if shared.StringInSlice(table, []string{"images", "networks", "storage_pools"}) {
				entity := table[:len(table)-1]
				importNodeAssociation(entity, columns, row, tx)
			}
		}
	}

	return tx.Commit()
}
func (c *Cluster) ImportPreClusteringData(dump *Dump) error {
	tx, err := c.db.Begin()
	if err != nil {
		return errors.Wrap(err, "failed to start cluster database transaction")
	}

	// Delete the default profile in the cluster database, which always
	// gets created no matter what.
	_, err = tx.Exec("DELETE FROM profiles WHERE id=1")
	if err != nil {
		tx.Rollback()
		return errors.Wrap(err, "failed to delete default profile")
	}

	for _, table := range preClusteringTables {
		logger.Debugf("Migrating data for table %s", table)

		for i, row := range dump.Data[table] {
			for i, element := range row {
				// Convert []byte columns to string. This is safe to do since
				// the pre-clustering schema only had TEXT fields and no BLOB.
				bytes, ok := element.([]byte)
				if ok {
					row[i] = string(bytes)
				}
			}
			columns := dump.Schema[table]

			nullNodeID := false // Whether node-related rows should have a NULL node ID
			appendNodeID := func() {
				columns = append(columns, "node_id")
				if nullNodeID {
					row = append(row, nil)
				} else {
					row = append(row, int64(1))
				}
			}

			switch table {
			case "config":
				// Don't migrate the core.https_address and maas.machine config keys,
				// which is node-specific and must remain in the node
				// database.
				keys := []string{"core.https_address", "maas.machine"}
				skip := false
				for i, column := range columns {
					value, ok := row[i].(string)
					if !ok {
						continue
					}
					if column == "key" && shared.StringInSlice(value, keys) {
						skip = true
					}
				}
				if skip {
					continue
				}
			case "containers":
				appendNodeID()
			case "networks_config":
				// The keys listed in NetworkNodeConfigKeys
				// are the only ones which are not global to the
				// cluster, so all other keys will have a NULL
				// node_id.
				index := 0
				for i, column := range columns {
					if column == "key" {
						index = i
						break
					}
				}
				key := row[index].(string)
				if !shared.StringInSlice(key, NetworkNodeConfigKeys) {
					nullNodeID = true
					break
				}
				appendNodeID()
			case "storage_pools_config":
				// The keys listed in StoragePoolNodeConfigKeys
				// are the only ones which are not global to the
				// cluster, so all other keys will have a NULL
				// node_id.
				index := 0
				for i, column := range columns {
					if column == "key" {
						index = i
						break
					}
				}
				key := row[index].(string)
				if !shared.StringInSlice(key, StoragePoolNodeConfigKeys) {
					nullNodeID = true
					break
				}
				appendNodeID()
			case "networks":
				fallthrough
			case "storage_pools":
				columns = append(columns, "state")
				row = append(row, storagePoolCreated)
			case "storage_volumes":
				appendNodeID()
			}

			if shared.StringInSlice(table, preClusteringTablesRequiringProjectID) {
				// These tables have a project_id reference in the new schema.
				columns = append(columns, "project_id")
				row = append(row, 1) // Reference the default project.
			}

			stmt := fmt.Sprintf("INSERT INTO %s(%s)", table, strings.Join(columns, ", "))
			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)
			}

			// Also insert the image ID to node ID association.
			if shared.StringInSlice(table, []string{"images", "networks", "storage_pools"}) {
				entity := table[:len(table)-1]
				importNodeAssociation(entity, columns, row, tx)
			}
		}
	}

	return tx.Commit()
}
// ImportPreClusteringData imports the data loaded with LoadPreClusteringData.