[lxc-devel] [lxd/master] Entity descriptions

albertodonato on Github lxc-bot at linuxcontainers.org
Wed May 3 09:59:48 UTC 2017


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 439 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20170503/765e6074/attachment.bin>
-------------- next part --------------
From c710a38186c5149d05922b9c194ace1154b0daae Mon Sep 17 00:00:00 2001
From: Alberto Donato <alberto.donato at canonical.com>
Date: Wed, 26 Apr 2017 11:05:07 +0200
Subject: [PATCH 1/6] Add description field to networks.

Signed-off-by: Alberto Donato <alberto.donato at canonical.com>
---
 doc/rest-api.md        |  1 +
 lxc/network.go         |  3 ++-
 lxd/db.go              |  1 +
 lxd/db_networks.go     | 23 ++++++++++++++++++-----
 lxd/db_update.go       |  6 ++++++
 lxd/networks.go        | 43 ++++++++++++++++++++++++-------------------
 shared/api/network.go  |  3 ++-
 test/suites/network.sh |  6 ++++++
 8 files changed, 60 insertions(+), 26 deletions(-)

diff --git a/doc/rest-api.md b/doc/rest-api.md
index 94a883c..8b92db3 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -1562,6 +1562,7 @@ Input:
 
     {
         "name": "my-network",
+        "description": "My network",
         "config": {
             "ipv4.address": "none",
             "ipv6.address": "2001:470:b368:4242::1/64",
diff --git a/lxc/network.go b/lxc/network.go
index 920a67e..0fdea24 100644
--- a/lxc/network.go
+++ b/lxc/network.go
@@ -447,7 +447,7 @@ func (c *networkCmd) doNetworkList(config *lxd.Config, args []string) error {
 		}
 
 		strUsedBy := fmt.Sprintf("%d", len(network.UsedBy))
-		data = append(data, []string{network.Name, network.Type, strManaged, strUsedBy})
+		data = append(data, []string{network.Name, network.Type, strManaged, network.Description, strUsedBy})
 	}
 
 	table := tablewriter.NewWriter(os.Stdout)
@@ -458,6 +458,7 @@ func (c *networkCmd) doNetworkList(config *lxd.Config, args []string) error {
 		i18n.G("NAME"),
 		i18n.G("TYPE"),
 		i18n.G("MANAGED"),
+		i18n.G("DESCRIPTION"),
 		i18n.G("USED BY")})
 	sort.Sort(byName(data))
 	table.AppendBulk(data)
diff --git a/lxd/db.go b/lxd/db.go
index 73216c6..b2df92b 100644
--- a/lxd/db.go
+++ b/lxd/db.go
@@ -128,6 +128,7 @@ CREATE TABLE IF NOT EXISTS images_source (
 CREATE TABLE IF NOT EXISTS networks (
     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
     name VARCHAR(255) NOT NULL,
+    description TEXT,
     UNIQUE (name)
 );
 CREATE TABLE IF NOT EXISTS networks_config (
diff --git a/lxd/db_networks.go b/lxd/db_networks.go
index 3040617..425edf6 100644
--- a/lxd/db_networks.go
+++ b/lxd/db_networks.go
@@ -29,11 +29,12 @@ func dbNetworks(db *sql.DB) ([]string, error) {
 }
 
 func dbNetworkGet(db *sql.DB, name string) (int64, *api.Network, error) {
+	description := sql.NullString{}
 	id := int64(-1)
 
-	q := "SELECT id FROM networks WHERE name=?"
+	q := "SELECT id, description FROM networks WHERE name=?"
 	arg1 := []interface{}{name}
-	arg2 := []interface{}{&id}
+	arg2 := []interface{}{&id, &description}
 	err := dbQueryRowScan(db, q, arg1, arg2)
 	if err != nil {
 		return -1, nil, err
@@ -49,6 +50,7 @@ func dbNetworkGet(db *sql.DB, name string) (int64, *api.Network, error) {
 		Managed: true,
 		Type:    "bridge",
 	}
+	network.Description = description.String
 	network.Config = config
 
 	return id, &network, nil
@@ -140,13 +142,13 @@ func dbNetworkConfigGet(db *sql.DB, id int64) (map[string]string, error) {
 	return config, nil
 }
 
-func dbNetworkCreate(db *sql.DB, name string, config map[string]string) (int64, error) {
+func dbNetworkCreate(db *sql.DB, name, description string, config map[string]string) (int64, error) {
 	tx, err := dbBegin(db)
 	if err != nil {
 		return -1, err
 	}
 
-	result, err := tx.Exec("INSERT INTO networks (name) VALUES (?)", name)
+	result, err := tx.Exec("INSERT INTO networks (name, description) VALUES (?, ?)", name, description)
 	if err != nil {
 		tx.Rollback()
 		return -1, err
@@ -172,7 +174,7 @@ func dbNetworkCreate(db *sql.DB, name string, config map[string]string) (int64,
 	return id, nil
 }
 
-func dbNetworkUpdate(db *sql.DB, name string, config map[string]string) error {
+func dbNetworkUpdate(db *sql.DB, name, description string, config map[string]string) error {
 	id, _, err := dbNetworkGet(db, name)
 	if err != nil {
 		return err
@@ -183,6 +185,12 @@ func dbNetworkUpdate(db *sql.DB, name string, config map[string]string) error {
 		return err
 	}
 
+	err = dbNetworkUpdateDescription(tx, id, description)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
 	err = dbNetworkConfigClear(tx, id)
 	if err != nil {
 		tx.Rollback()
@@ -198,6 +206,11 @@ func dbNetworkUpdate(db *sql.DB, name string, config map[string]string) error {
 	return txCommit(tx)
 }
 
+func dbNetworkUpdateDescription(tx *sql.Tx, id int64, description string) error {
+	_, err := tx.Exec("UPDATE networks SET description=? WHERE id=?", description, id)
+	return err
+}
+
 func dbNetworkConfigAdd(tx *sql.Tx, id int64, config map[string]string) error {
 	str := fmt.Sprintf("INSERT INTO networks_config (network_id, key, value) VALUES(?, ?, ?)")
 	stmt, err := tx.Prepare(str)
diff --git a/lxd/db_update.go b/lxd/db_update.go
index ee1dc71..72c60d5 100644
--- a/lxd/db_update.go
+++ b/lxd/db_update.go
@@ -69,6 +69,7 @@ var dbUpdates = []dbUpdate{
 	{version: 33, run: dbUpdateFromV32},
 	{version: 34, run: dbUpdateFromV33},
 	{version: 35, run: dbUpdateFromV34},
+	{version: 36, run: dbUpdateFromV35},
 }
 
 type dbUpdate struct {
@@ -125,6 +126,11 @@ func dbUpdatesApplyAll(d *Daemon) error {
 }
 
 // Schema updates begin here
+func dbUpdateFromV35(currentVersion int, version int, d *Daemon) error {
+	_, err := d.db.Exec("ALTER TABLE networks ADD COLUMN description TEXT;")
+	return err
+}
+
 func dbUpdateFromV34(currentVersion int, version int, d *Daemon) error {
 	stmt := `
 CREATE TABLE IF NOT EXISTS storage_pools (
diff --git a/lxd/networks.go b/lxd/networks.go
index 1f54977..cefcb38 100644
--- a/lxd/networks.go
+++ b/lxd/networks.go
@@ -126,7 +126,7 @@ func networksPost(d *Daemon, r *http.Request) Response {
 	}
 
 	// Create the database entry
-	_, err = dbNetworkCreate(d.db, req.Name, req.Config)
+	_, err = dbNetworkCreate(d.db, req.Name, req.Description, req.Config)
 	if err != nil {
 		return InternalError(
 			fmt.Errorf("Error inserting %s into database: %s", req.Name, err))
@@ -157,7 +157,7 @@ func networkGet(d *Daemon, r *http.Request) Response {
 		return SmartError(err)
 	}
 
-	etag := []interface{}{n.Name, n.Managed, n.Type, n.Config}
+	etag := []interface{}{n.Name, n.Description, n.Managed, n.Type, n.Config}
 
 	return SyncResponseETag(true, &n, etag)
 }
@@ -201,6 +201,7 @@ func doNetworkGet(d *Daemon, name string) (api.Network, error) {
 	} else if dbInfo != nil || shared.PathExists(fmt.Sprintf("/sys/class/net/%s/bridge", n.Name)) {
 		if dbInfo != nil {
 			n.Managed = true
+			n.Description = dbInfo.Description
 			n.Config = dbInfo.Config
 		}
 
@@ -301,7 +302,7 @@ func networkPut(d *Daemon, r *http.Request) Response {
 	}
 
 	// Validate the ETag
-	etag := []interface{}{dbInfo.Name, dbInfo.Managed, dbInfo.Type, dbInfo.Config}
+	etag := []interface{}{dbInfo.Name, dbInfo.Managed, dbInfo.Type, dbInfo.Description, dbInfo.Config}
 
 	err = etagCheck(r, etag)
 	if err != nil {
@@ -313,7 +314,7 @@ func networkPut(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 
-	return doNetworkUpdate(d, name, dbInfo.Config, req.Config)
+	return doNetworkUpdate(d, name, dbInfo.Config, req)
 }
 
 func networkPatch(d *Daemon, r *http.Request) Response {
@@ -326,7 +327,7 @@ func networkPatch(d *Daemon, r *http.Request) Response {
 	}
 
 	// Validate the ETag
-	etag := []interface{}{dbInfo.Name, dbInfo.Managed, dbInfo.Type, dbInfo.Config}
+	etag := []interface{}{dbInfo.Name, dbInfo.Managed, dbInfo.Type, dbInfo.Description, dbInfo.Config}
 
 	err = etagCheck(r, etag)
 	if err != nil {
@@ -350,20 +351,20 @@ func networkPatch(d *Daemon, r *http.Request) Response {
 		}
 	}
 
-	return doNetworkUpdate(d, name, dbInfo.Config, req.Config)
+	return doNetworkUpdate(d, name, dbInfo.Config, req)
 }
 
-func doNetworkUpdate(d *Daemon, name string, oldConfig map[string]string, newConfig map[string]string) Response {
+func doNetworkUpdate(d *Daemon, name string, oldConfig map[string]string, req api.NetworkPut) Response {
 	// Validate the configuration
-	err := networkValidateConfig(name, newConfig)
+	err := networkValidateConfig(name, req.Config)
 	if err != nil {
 		return BadRequest(err)
 	}
 
 	// When switching to a fan bridge, auto-detect the underlay
-	if newConfig["bridge.mode"] == "fan" {
-		if newConfig["fan.underlay_subnet"] == "" {
-			newConfig["fan.underlay_subnet"] = "auto"
+	if req.Config["bridge.mode"] == "fan" {
+		if req.Config["fan.underlay_subnet"] == "" {
+			req.Config["fan.underlay_subnet"] = "auto"
 		}
 	}
 
@@ -373,7 +374,7 @@ func doNetworkUpdate(d *Daemon, name string, oldConfig map[string]string, newCon
 		return NotFound
 	}
 
-	err = n.Update(api.NetworkPut{Config: newConfig})
+	err = n.Update(req)
 	if err != nil {
 		return SmartError(err)
 	}
@@ -390,7 +391,7 @@ func networkLoadByName(d *Daemon, name string) (*network, error) {
 		return nil, err
 	}
 
-	n := network{daemon: d, id: id, name: name, config: dbInfo.Config}
+	n := network{daemon: d, id: id, name: name, description: dbInfo.Description, config: dbInfo.Config}
 
 	return &n, nil
 }
@@ -421,9 +422,10 @@ func networkStartup(d *Daemon) error {
 
 type network struct {
 	// Properties
-	daemon *Daemon
-	id     int64
-	name   string
+	daemon      *Daemon
+	id          int64
+	name        string
+	description string
 
 	// config
 	config map[string]string
@@ -1271,6 +1273,7 @@ func (n *network) Update(newNetwork api.NetworkPut) error {
 
 	// Backup the current state
 	oldConfig := map[string]string{}
+	oldDescription := n.description
 	err = shared.DeepCopy(&n.config, &oldConfig)
 	if err != nil {
 		return err
@@ -1284,6 +1287,7 @@ func (n *network) Update(newNetwork api.NetworkPut) error {
 	defer func() {
 		if undoChanges {
 			n.config = oldConfig
+			n.description = oldDescription
 		}
 	}()
 
@@ -1315,7 +1319,7 @@ func (n *network) Update(newNetwork api.NetworkPut) error {
 	}
 
 	// Skip on no change
-	if len(changedConfig) == 0 {
+	if len(changedConfig) == 0 && newNetwork.Description == n.description {
 		return nil
 	}
 
@@ -1351,11 +1355,12 @@ func (n *network) Update(newNetwork api.NetworkPut) error {
 		}
 	}
 
-	// Apply the new configuration
+	// Apply changes
 	n.config = newConfig
+	n.description = newNetwork.Description
 
 	// Update the database
-	err = dbNetworkUpdate(n.daemon.db, n.name, n.config)
+	err = dbNetworkUpdate(n.daemon.db, n.name, n.description, n.config)
 	if err != nil {
 		return err
 	}
diff --git a/shared/api/network.go b/shared/api/network.go
index 21db460..ac749d8 100644
--- a/shared/api/network.go
+++ b/shared/api/network.go
@@ -22,7 +22,8 @@ type NetworkPost struct {
 //
 // API extension: network
 type NetworkPut struct {
-	Config map[string]string `json:"config" yaml:"config"`
+	Description string            `json:"description" yaml:"description"`
+	Config      map[string]string `json:"config" yaml:"config"`
 }
 
 // Network represents a LXD network
diff --git a/test/suites/network.sh b/test/suites/network.sh
index 9c849f1..69432cf 100644
--- a/test/suites/network.sh
+++ b/test/suites/network.sh
@@ -13,6 +13,12 @@ test_network() {
   lxc network set lxdt$$ ipv6.dhcp.stateful true
   lxc network delete lxdt$$
 
+  # edit network description
+  lxc network create lxdt$$
+  lxc network show lxdt$$ | sed 's/^description:.*/description: foo/' | lxc network edit lxdt$$
+  lxc network show lxdt$$ | grep -q 'description: foo'
+  lxc network delete lxdt$$
+
   # Unconfigured bridge
   lxc network create lxdt$$ ipv4.address=none ipv6.address=none
   lxc network delete lxdt$$

From ddb1aba332e27cc6c481884ef1ad5236b8868cc4 Mon Sep 17 00:00:00 2001
From: Alberto Donato <alberto.donato at canonical.com>
Date: Thu, 27 Apr 2017 18:36:50 +0200
Subject: [PATCH 2/6] Change image description type to text.

Signed-off-by: Alberto Donato <alberto.donato at canonical.com>
---
 lxd/db.go        |  2 +-
 lxd/db_update.go | 21 +++++++++++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/lxd/db.go b/lxd/db.go
index b2df92b..6662552 100644
--- a/lxd/db.go
+++ b/lxd/db.go
@@ -104,7 +104,7 @@ CREATE TABLE IF NOT EXISTS images_aliases (
     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
     name VARCHAR(255) NOT NULL,
     image_id INTEGER NOT NULL,
-    description VARCHAR(255),
+    description TEXT,
     FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE,
     UNIQUE (name)
 );
diff --git a/lxd/db_update.go b/lxd/db_update.go
index 72c60d5..369f4a2 100644
--- a/lxd/db_update.go
+++ b/lxd/db_update.go
@@ -70,6 +70,7 @@ var dbUpdates = []dbUpdate{
 	{version: 34, run: dbUpdateFromV33},
 	{version: 35, run: dbUpdateFromV34},
 	{version: 36, run: dbUpdateFromV35},
+	{version: 37, run: dbUpdateFromV36},
 }
 
 type dbUpdate struct {
@@ -126,6 +127,26 @@ func dbUpdatesApplyAll(d *Daemon) error {
 }
 
 // Schema updates begin here
+func dbUpdateFromV36(currentVersion int, version int, d *Daemon) error {
+	stmt := `
+CREATE TABLE tmp (
+    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+    name VARCHAR(255) NOT NULL,
+    image_id INTEGER NOT NULL,
+    description TEXT,
+    FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE,
+    UNIQUE (name)
+);
+INSERT INTO tmp (id, name, image_id, description)
+    SELECT id, name, image_id, description
+    FROM images_aliases;
+DROP TABLE images_aliases;
+ALTER TABLE tmp RENAME TO images_aliases;
+`
+	_, err := d.db.Exec(stmt)
+	return err
+}
+
 func dbUpdateFromV35(currentVersion int, version int, d *Daemon) error {
 	_, err := d.db.Exec("ALTER TABLE networks ADD COLUMN description TEXT;")
 	return err

From 5c05714de95b70f1aadc689c715725d0723fd0bf Mon Sep 17 00:00:00 2001
From: Alberto Donato <alberto.donato at canonical.com>
Date: Fri, 28 Apr 2017 15:54:53 +0200
Subject: [PATCH 3/6] Add description field to storage pools.

Signed-off-by: Alberto Donato <alberto.donato at canonical.com>
---
 lxc/storage.go             |  3 ++-
 lxd/api_internal.go        |  2 +-
 lxd/db.go                  |  1 +
 lxd/db_storage_pools.go    | 25 ++++++++++++++++-----
 lxd/db_update.go           |  6 ++++++
 lxd/main_test.go           |  4 +++-
 lxd/patches.go             | 22 +++++++++----------
 lxd/storage_pools.go       |  6 +++---
 lxd/storage_pools_utils.go | 54 ++++++++++++++++++++++++----------------------
 shared/api/storage.go      |  3 ++-
 test/suites/storage.sh     | 11 ++++++++++
 11 files changed, 88 insertions(+), 49 deletions(-)

diff --git a/lxc/storage.go b/lxc/storage.go
index 0899c2e..5f71d6e 100644
--- a/lxc/storage.go
+++ b/lxc/storage.go
@@ -596,7 +596,7 @@ func (c *storageCmd) doStoragePoolsList(config *lxd.Config, args []string) error
 	for _, pool := range pools {
 		usedby := strconv.Itoa(len(pool.UsedBy))
 
-		data = append(data, []string{pool.Name, pool.Driver, pool.Config["source"], usedby})
+		data = append(data, []string{pool.Name, pool.Description, pool.Driver, pool.Config["source"], usedby})
 	}
 
 	table := tablewriter.NewWriter(os.Stdout)
@@ -605,6 +605,7 @@ func (c *storageCmd) doStoragePoolsList(config *lxd.Config, args []string) error
 	table.SetRowLine(true)
 	table.SetHeader([]string{
 		i18n.G("NAME"),
+		i18n.G("DESCRIPTION"),
 		i18n.G("DRIVER"),
 		i18n.G("SOURCE"),
 		i18n.G("USED BY")})
diff --git a/lxd/api_internal.go b/lxd/api_internal.go
index 2298d84..11d3655 100644
--- a/lxd/api_internal.go
+++ b/lxd/api_internal.go
@@ -204,7 +204,7 @@ func internalImport(d *Daemon, r *http.Request) Response {
 
 	if poolErr == NoSuchObjectError {
 		// Create the storage pool db entry if it doesn't exist.
-		err := storagePoolDBCreate(d, containerPoolName, backup.Pool.Driver, backup.Pool.Config)
+		err := storagePoolDBCreate(d, containerPoolName, pool.Description, backup.Pool.Driver, backup.Pool.Config)
 		if err != nil {
 			return InternalError(err)
 		}
diff --git a/lxd/db.go b/lxd/db.go
index 6662552..1e5d0a2 100644
--- a/lxd/db.go
+++ b/lxd/db.go
@@ -184,6 +184,7 @@ CREATE TABLE IF NOT EXISTS schema (
 CREATE TABLE IF NOT EXISTS storage_pools (
     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
     name VARCHAR(255) NOT NULL,
+    description TEXT,
     driver VARCHAR(255) NOT NULL,
     UNIQUE (name)
 );
diff --git a/lxd/db_storage_pools.go b/lxd/db_storage_pools.go
index a98b51c..f155b8e 100644
--- a/lxd/db_storage_pools.go
+++ b/lxd/db_storage_pools.go
@@ -78,9 +78,11 @@ func dbStoragePoolGetID(db *sql.DB, poolName string) (int64, error) {
 func dbStoragePoolGet(db *sql.DB, poolName string) (int64, *api.StoragePool, error) {
 	var poolDriver string
 	poolID := int64(-1)
-	query := "SELECT id, driver FROM storage_pools WHERE name=?"
+	description := sql.NullString{}
+
+	query := "SELECT id, driver, description FROM storage_pools WHERE name=?"
 	inargs := []interface{}{poolName}
-	outargs := []interface{}{&poolID, &poolDriver}
+	outargs := []interface{}{&poolID, &poolDriver, &description}
 
 	err := dbQueryRowScan(db, query, inargs, outargs)
 	if err != nil {
@@ -96,6 +98,7 @@ func dbStoragePoolGet(db *sql.DB, poolName string) (int64, *api.StoragePool, err
 		Name:   poolName,
 		Driver: poolDriver,
 	}
+	storagePool.Description = description.String
 	storagePool.Config = config
 
 	return poolID, &storagePool, nil
@@ -126,13 +129,13 @@ func dbStoragePoolConfigGet(db *sql.DB, poolID int64) (map[string]string, error)
 }
 
 // Create new storage pool.
-func dbStoragePoolCreate(db *sql.DB, poolName string, poolDriver string, poolConfig map[string]string) (int64, error) {
+func dbStoragePoolCreate(db *sql.DB, poolName, poolDescription string, poolDriver string, poolConfig map[string]string) (int64, error) {
 	tx, err := dbBegin(db)
 	if err != nil {
 		return -1, err
 	}
 
-	result, err := tx.Exec("INSERT INTO storage_pools (name, driver) VALUES (?, ?)", poolName, poolDriver)
+	result, err := tx.Exec("INSERT INTO storage_pools (name, description, driver) VALUES (?, ?, ?)", poolName, poolDescription, poolDriver)
 	if err != nil {
 		tx.Rollback()
 		return -1, err
@@ -188,7 +191,7 @@ func dbStoragePoolConfigAdd(tx *sql.Tx, poolID int64, poolConfig map[string]stri
 }
 
 // Update storage pool.
-func dbStoragePoolUpdate(db *sql.DB, poolName string, poolConfig map[string]string) error {
+func dbStoragePoolUpdate(db *sql.DB, poolName, description string, poolConfig map[string]string) error {
 	poolID, _, err := dbStoragePoolGet(db, poolName)
 	if err != nil {
 		return err
@@ -199,6 +202,12 @@ func dbStoragePoolUpdate(db *sql.DB, poolName string, poolConfig map[string]stri
 		return err
 	}
 
+	err = dbStoragePoolUpdateDescription(tx, poolID, description)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
 	err = dbStoragePoolConfigClear(tx, poolID)
 	if err != nil {
 		tx.Rollback()
@@ -214,6 +223,12 @@ func dbStoragePoolUpdate(db *sql.DB, poolName string, poolConfig map[string]stri
 	return txCommit(tx)
 }
 
+// Update the storage pool description.
+func dbStoragePoolUpdateDescription(tx *sql.Tx, id int64, description string) error {
+	_, err := tx.Exec("UPDATE storage_pools SET description=? WHERE id=?", description, id)
+	return err
+}
+
 // Delete storage pool config.
 func dbStoragePoolConfigClear(tx *sql.Tx, poolID int64) error {
 	_, err := tx.Exec("DELETE FROM storage_pools_config WHERE storage_pool_id=?", poolID)
diff --git a/lxd/db_update.go b/lxd/db_update.go
index 369f4a2..30fc59b 100644
--- a/lxd/db_update.go
+++ b/lxd/db_update.go
@@ -71,6 +71,7 @@ var dbUpdates = []dbUpdate{
 	{version: 35, run: dbUpdateFromV34},
 	{version: 36, run: dbUpdateFromV35},
 	{version: 37, run: dbUpdateFromV36},
+	{version: 38, run: dbUpdateFromV37},
 }
 
 type dbUpdate struct {
@@ -127,6 +128,11 @@ func dbUpdatesApplyAll(d *Daemon) error {
 }
 
 // Schema updates begin here
+func dbUpdateFromV37(currentVersion int, version int, d *Daemon) error {
+	_, err := d.db.Exec("ALTER TABLE storage_pools ADD COLUMN description TEXT;")
+	return err
+}
+
 func dbUpdateFromV36(currentVersion int, version int, d *Daemon) error {
 	stmt := `
 CREATE TABLE tmp (
diff --git a/lxd/main_test.go b/lxd/main_test.go
index a6828c8..af0d88e 100644
--- a/lxd/main_test.go
+++ b/lxd/main_test.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"crypto/tls"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"testing"
@@ -61,7 +62,8 @@ func (suite *lxdTestSuite) SetupSuite() {
 
 	mockStorage, _ := storageTypeToString(storageTypeMock)
 	// Create the database entry for the storage pool.
-	_, err = dbStoragePoolCreate(suite.d.db, lxdTestSuiteDefaultStoragePool, mockStorage, poolConfig)
+	poolDescription := fmt.Sprintf("%s storage pool", lxdTestSuiteDefaultStoragePool)
+	_, err = dbStoragePoolCreate(suite.d.db, lxdTestSuiteDefaultStoragePool, poolDescription, mockStorage, poolConfig)
 	if err != nil {
 		os.Exit(1)
 	}
diff --git a/lxd/patches.go b/lxd/patches.go
index 7bf8abc..837159c 100644
--- a/lxd/patches.go
+++ b/lxd/patches.go
@@ -310,12 +310,12 @@ func upgradeFromStorageTypeBtrfs(name string, d *Daemon, defaultPoolName string,
 		if pool.Config == nil {
 			pool.Config = poolConfig
 		}
-		err = dbStoragePoolUpdate(d.db, defaultPoolName, pool.Config)
+		err = dbStoragePoolUpdate(d.db, defaultPoolName, "", pool.Config)
 		if err != nil {
 			return err
 		}
 	} else if err == NoSuchObjectError { // Likely a pristine upgrade.
-		tmp, err := dbStoragePoolCreate(d.db, defaultPoolName, defaultStorageTypeName, poolConfig)
+		tmp, err := dbStoragePoolCreate(d.db, defaultPoolName, "", defaultStorageTypeName, poolConfig)
 		if err != nil {
 			return err
 		}
@@ -607,12 +607,12 @@ func upgradeFromStorageTypeDir(name string, d *Daemon, defaultPoolName string, d
 		if pool.Config == nil {
 			pool.Config = poolConfig
 		}
-		err = dbStoragePoolUpdate(d.db, defaultPoolName, pool.Config)
+		err = dbStoragePoolUpdate(d.db, defaultPoolName, pool.Description, pool.Config)
 		if err != nil {
 			return err
 		}
 	} else if err == NoSuchObjectError { // Likely a pristine upgrade.
-		tmp, err := dbStoragePoolCreate(d.db, defaultPoolName, defaultStorageTypeName, poolConfig)
+		tmp, err := dbStoragePoolCreate(d.db, defaultPoolName, "", defaultStorageTypeName, poolConfig)
 		if err != nil {
 			return err
 		}
@@ -900,12 +900,12 @@ func upgradeFromStorageTypeLvm(name string, d *Daemon, defaultPoolName string, d
 		if pool.Config == nil {
 			pool.Config = poolConfig
 		}
-		err = dbStoragePoolUpdate(d.db, defaultPoolName, pool.Config)
+		err = dbStoragePoolUpdate(d.db, defaultPoolName, pool.Description, pool.Config)
 		if err != nil {
 			return err
 		}
 	} else if err == NoSuchObjectError { // Likely a pristine upgrade.
-		tmp, err := dbStoragePoolCreate(d.db, defaultPoolName, defaultStorageTypeName, poolConfig)
+		tmp, err := dbStoragePoolCreate(d.db, defaultPoolName, "", defaultStorageTypeName, poolConfig)
 		if err != nil {
 			return err
 		}
@@ -1400,7 +1400,7 @@ func upgradeFromStorageTypeZfs(name string, d *Daemon, defaultPoolName string, d
 		if pool.Config == nil {
 			pool.Config = poolConfig
 		}
-		err = dbStoragePoolUpdate(d.db, poolName, pool.Config)
+		err = dbStoragePoolUpdate(d.db, poolName, pool.Description, pool.Config)
 		if err != nil {
 			return err
 		}
@@ -1432,7 +1432,7 @@ func upgradeFromStorageTypeZfs(name string, d *Daemon, defaultPoolName string, d
 		}
 
 		// (Use a tmp variable as Go's scoping is freaking me out.)
-		tmp, err := dbStoragePoolCreate(d.db, poolName, defaultStorageTypeName, poolConfig)
+		tmp, err := dbStoragePoolCreate(d.db, poolName, defaultStorageTypeName, "", poolConfig)
 		if err != nil {
 			logger.Warnf("Storage pool already exists in the database. Proceeding...")
 		}
@@ -1929,7 +1929,7 @@ func patchStorageApiKeys(name string, d *Daemon) error {
 		}
 
 		// Update the config in the database.
-		err = dbStoragePoolUpdate(d.db, poolName, pool.Config)
+		err = dbStoragePoolUpdate(d.db, poolName, pool.Description, pool.Config)
 		if err != nil {
 			return err
 		}
@@ -2017,7 +2017,7 @@ func patchStorageApiUpdateStorageConfigs(name string, d *Daemon) error {
 		}
 
 		// Update the storage pool config.
-		err = dbStoragePoolUpdate(d.db, poolName, pool.Config)
+		err = dbStoragePoolUpdate(d.db, poolName, pool.Description, pool.Config)
 		if err != nil {
 			return err
 		}
@@ -2134,7 +2134,7 @@ func patchStorageApiLxdOnBtrfs(name string, d *Daemon) error {
 		pool.Config["source"] = getStoragePoolMountPoint(poolName)
 
 		// Update the storage pool config.
-		err = dbStoragePoolUpdate(d.db, poolName, pool.Config)
+		err = dbStoragePoolUpdate(d.db, poolName, pool.Description, pool.Config)
 		if err != nil {
 			return err
 		}
diff --git a/lxd/storage_pools.go b/lxd/storage_pools.go
index bbd23f0..3ee5fe4 100644
--- a/lxd/storage_pools.go
+++ b/lxd/storage_pools.go
@@ -75,7 +75,7 @@ func storagePoolsPost(d *Daemon, r *http.Request) Response {
 		return BadRequest(fmt.Errorf("No driver provided"))
 	}
 
-	err = storagePoolCreateInternal(d, req.Name, req.Driver, req.Config)
+	err = storagePoolCreateInternal(d, req.Name, req.Description, req.Driver, req.Config)
 	if err != nil {
 		return InternalError(err)
 	}
@@ -138,7 +138,7 @@ func storagePoolPut(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 
-	err = storagePoolUpdate(d, poolName, req.Config)
+	err = storagePoolUpdate(d, poolName, req.Description, req.Config)
 	if err != nil {
 		return InternalError(err)
 	}
@@ -188,7 +188,7 @@ func storagePoolPatch(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 
-	err = storagePoolUpdate(d, poolName, req.Config)
+	err = storagePoolUpdate(d, poolName, req.Description, req.Config)
 	if err != nil {
 		return InternalError(fmt.Errorf("failed to update the storage pool configuration"))
 	}
diff --git a/lxd/storage_pools_utils.go b/lxd/storage_pools_utils.go
index bcac4d1..95761c4 100644
--- a/lxd/storage_pools_utils.go
+++ b/lxd/storage_pools_utils.go
@@ -9,7 +9,7 @@ import (
 	"github.com/lxc/lxd/shared/version"
 )
 
-func storagePoolUpdate(d *Daemon, name string, newConfig map[string]string) error {
+func storagePoolUpdate(d *Daemon, name, newDescription string, newConfig map[string]string) error {
 	s, err := storagePoolInit(d, name)
 	if err != nil {
 		return err
@@ -19,6 +19,7 @@ func storagePoolUpdate(d *Daemon, name string, newConfig map[string]string) erro
 	newWritable := oldWritable
 
 	// Backup the current state
+	oldDescription := oldWritable.Description
 	oldConfig := map[string]string{}
 	err = shared.DeepCopy(&oldWritable.Config, &oldConfig)
 	if err != nil {
@@ -37,34 +38,35 @@ func storagePoolUpdate(d *Daemon, name string, newConfig map[string]string) erro
 	}()
 
 	changedConfig, userOnly := storageConfigDiff(oldConfig, newConfig)
-	// Skip on no change
-	if len(changedConfig) == 0 {
-		return nil
-	}
-
-	newWritable.Config = newConfig
+	// Apply config changes if there are any
+	if len(changedConfig) != 0 {
+		newWritable.Description = newDescription
+		newWritable.Config = newConfig
+
+		// Update the storage pool
+		if !userOnly {
+			if shared.StringInSlice("driver", changedConfig) {
+				return fmt.Errorf("the \"driver\" property of a storage pool cannot be changed")
+			}
 
-	// Update the storage pool
-	if !userOnly {
-		if shared.StringInSlice("driver", changedConfig) {
-			return fmt.Errorf("the \"driver\" property of a storage pool cannot be changed")
+			err = s.StoragePoolUpdate(&newWritable, changedConfig)
+			if err != nil {
+				return err
+			}
 		}
 
-		err = s.StoragePoolUpdate(&newWritable, changedConfig)
+		// Apply the new configuration
+		s.SetStoragePoolWritable(&newWritable)
+	}
+
+	// Update the database if something changed
+	if len(changedConfig) != 0 || newDescription != oldDescription {
+		err = dbStoragePoolUpdate(d.db, name, newDescription, newConfig)
 		if err != nil {
 			return err
 		}
 	}
 
-	// Apply the new configuration
-	s.SetStoragePoolWritable(&newWritable)
-
-	// Update the database
-	err = dbStoragePoolUpdate(d.db, name, newConfig)
-	if err != nil {
-		return err
-	}
-
 	// Success, update the closure to mark that the changes should be kept.
 	undoChanges = false
 
@@ -153,7 +155,7 @@ func profilesUsingPoolGetNames(db *sql.DB, poolName string) ([]string, error) {
 	return usedBy, nil
 }
 
-func storagePoolDBCreate(d *Daemon, poolName string, driver string, config map[string]string) error {
+func storagePoolDBCreate(d *Daemon, poolName, poolDescription string, driver string, config map[string]string) error {
 	// Check if the storage pool name is valid.
 	err := storageValidName(poolName)
 	if err != nil {
@@ -184,7 +186,7 @@ func storagePoolDBCreate(d *Daemon, poolName string, driver string, config map[s
 	}
 
 	// Create the database entry for the storage pool.
-	_, err = dbStoragePoolCreate(d.db, poolName, driver, config)
+	_, err = dbStoragePoolCreate(d.db, poolName, poolDescription, driver, config)
 	if err != nil {
 		return fmt.Errorf("Error inserting %s into database: %s", poolName, err)
 	}
@@ -192,8 +194,8 @@ func storagePoolDBCreate(d *Daemon, poolName string, driver string, config map[s
 	return nil
 }
 
-func storagePoolCreateInternal(d *Daemon, poolName string, driver string, config map[string]string) error {
-	err := storagePoolDBCreate(d, poolName, driver, config)
+func storagePoolCreateInternal(d *Daemon, poolName, poolDescription string, driver string, config map[string]string) error {
+	err := storagePoolDBCreate(d, poolName, poolDescription, driver, config)
 	if err != nil {
 		return err
 	}
@@ -235,7 +237,7 @@ func storagePoolCreateInternal(d *Daemon, poolName string, driver string, config
 	configDiff, _ := storageConfigDiff(config, postCreateConfig)
 	if len(configDiff) > 0 {
 		// Create the database entry for the storage pool.
-		err = dbStoragePoolUpdate(d.db, poolName, postCreateConfig)
+		err = dbStoragePoolUpdate(d.db, poolName, poolDescription, postCreateConfig)
 		if err != nil {
 			return fmt.Errorf("Error inserting %s into database: %s", poolName, err)
 		}
diff --git a/shared/api/storage.go b/shared/api/storage.go
index fac6d85..297b28e 100644
--- a/shared/api/storage.go
+++ b/shared/api/storage.go
@@ -25,7 +25,8 @@ type StoragePool struct {
 //
 // API extension: storage
 type StoragePoolPut struct {
-	Config map[string]string `json:"config" yaml:"config"`
+	Description string            `json:"description" yaml:"description"`
+	Config      map[string]string `json:"config" yaml:"config"`
 }
 
 // StorageVolumesPost represents the fields of a new LXD storage pool volume
diff --git a/test/suites/storage.sh b/test/suites/storage.sh
index 2ddbb46..44eb939 100644
--- a/test/suites/storage.sh
+++ b/test/suites/storage.sh
@@ -6,6 +6,17 @@ test_storage() {
   LXD_STORAGE_DIR=$(mktemp -d -p "${TEST_DIR}" XXXXXXXXX)
   chmod +x "${LXD_STORAGE_DIR}"
   spawn_lxd "${LXD_STORAGE_DIR}" false
+
+  # edit storage description
+
+  # shellcheck disable=2039
+  local storage_pool
+  storage_pool="lxdtest-$(basename "${LXD_DIR}")-pool"
+  lxc storage create "$storage_pool" "$lxd_backend"
+  lxc storage show "$storage_pool" | sed 's/^description:.*/description: foo/' | lxc storage edit "$storage_pool"
+  lxc storage show "$storage_pool" | grep -q 'description: foo'
+  lxc storage delete "$storage_pool"
+
   (
     set -e
     # shellcheck disable=2030

From b209dc9211c430eb4641d54e302ca0dcf382b0c7 Mon Sep 17 00:00:00 2001
From: Alberto Donato <alberto.donato at canonical.com>
Date: Tue, 2 May 2017 09:38:03 +0200
Subject: [PATCH 4/6] Add description field to storage volumes.

Signed-off-by: Alberto Donato <alberto.donato at canonical.com>
---
 lxc/storage.go               |  3 ++-
 lxd/container_lxc.go         |  2 +-
 lxd/db.go                    |  1 +
 lxd/db_storage_pools.go      | 19 +++++++++++++---
 lxd/db_storage_volumes.go    | 23 ++++++++++++++++++++
 lxd/db_update.go             |  6 +++++
 lxd/patches.go               | 52 ++++++++++++++++++++++----------------------
 lxd/storage_shared.go        |  2 +-
 lxd/storage_volumes.go       |  6 ++---
 lxd/storage_volumes_utils.go | 46 ++++++++++++++++++++-------------------
 shared/api/storage.go        |  4 ++--
 test/suites/storage.sh       | 13 +++++++----
 12 files changed, 114 insertions(+), 63 deletions(-)

diff --git a/lxc/storage.go b/lxc/storage.go
index 5f71d6e..50bcc2a 100644
--- a/lxc/storage.go
+++ b/lxc/storage.go
@@ -680,7 +680,7 @@ func (c *storageCmd) doStoragePoolVolumesList(config *lxd.Config, remote string,
 	data := [][]string{}
 	for _, volume := range volumes {
 		usedby := strconv.Itoa(len(volume.UsedBy))
-		data = append(data, []string{volume.Type, volume.Name, usedby})
+		data = append(data, []string{volume.Type, volume.Name, volume.Description, usedby})
 	}
 
 	table := tablewriter.NewWriter(os.Stdout)
@@ -690,6 +690,7 @@ func (c *storageCmd) doStoragePoolVolumesList(config *lxd.Config, remote string,
 	table.SetHeader([]string{
 		i18n.G("TYPE"),
 		i18n.G("NAME"),
+		i18n.G("DESCRIPTION"),
 		i18n.G("USED BY")})
 	sort.Sort(byNameAndType(data))
 	table.AppendBulk(data)
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 406368c..ac362e3 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -255,7 +255,7 @@ func containerLXCCreate(d *Daemon, args containerArgs) (container, error) {
 	}
 
 	// Create a new database entry for the container's storage volume
-	_, err = dbStoragePoolVolumeCreate(d.db, args.Name, storagePoolVolumeTypeContainer, poolID, volumeConfig)
+	_, err = dbStoragePoolVolumeCreate(d.db, args.Name, "", storagePoolVolumeTypeContainer, poolID, volumeConfig)
 	if err != nil {
 		c.Delete()
 		return nil, err
diff --git a/lxd/db.go b/lxd/db.go
index 1e5d0a2..b51497a 100644
--- a/lxd/db.go
+++ b/lxd/db.go
@@ -199,6 +199,7 @@ CREATE TABLE IF NOT EXISTS storage_pools_config (
 CREATE TABLE IF NOT EXISTS storage_volumes (
     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
     name VARCHAR(255) NOT NULL,
+    description TEXT,
     storage_pool_id INTEGER NOT NULL,
     type INTEGER NOT NULL,
     UNIQUE (storage_pool_id, name, type),
diff --git a/lxd/db_storage_pools.go b/lxd/db_storage_pools.go
index f155b8e..ad8261f 100644
--- a/lxd/db_storage_pools.go
+++ b/lxd/db_storage_pools.go
@@ -346,6 +346,11 @@ func dbStoragePoolVolumeGetType(db *sql.DB, volumeName string, volumeType int, p
 		return -1, nil, err
 	}
 
+	volumeDescription, err := dbStorageVolumeDescriptionGet(db, volumeID)
+	if err != nil {
+		return -1, nil, err
+	}
+
 	volumeTypeName, err := storagePoolVolumeTypeToName(volumeType)
 	if err != nil {
 		return -1, nil, err
@@ -355,13 +360,14 @@ func dbStoragePoolVolumeGetType(db *sql.DB, volumeName string, volumeType int, p
 		Type: volumeTypeName,
 	}
 	storageVolume.Name = volumeName
+	storageVolume.Description = volumeDescription
 	storageVolume.Config = volumeConfig
 
 	return volumeID, &storageVolume, nil
 }
 
 // Update storage volume attached to a given storage pool.
-func dbStoragePoolVolumeUpdate(db *sql.DB, volumeName string, volumeType int, poolID int64, volumeConfig map[string]string) error {
+func dbStoragePoolVolumeUpdate(db *sql.DB, volumeName string, volumeType int, poolID int64, volumeDescription string, volumeConfig map[string]string) error {
 	volumeID, _, err := dbStoragePoolVolumeGetType(db, volumeName, volumeType, poolID)
 	if err != nil {
 		return err
@@ -384,6 +390,12 @@ func dbStoragePoolVolumeUpdate(db *sql.DB, volumeName string, volumeType int, po
 		return err
 	}
 
+	err = dbStorageVolumeDescriptionUpdate(tx, volumeID, volumeDescription)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
 	return txCommit(tx)
 }
 
@@ -424,13 +436,14 @@ func dbStoragePoolVolumeRename(db *sql.DB, oldVolumeName string, newVolumeName s
 }
 
 // Create new storage volume attached to a given storage pool.
-func dbStoragePoolVolumeCreate(db *sql.DB, volumeName string, volumeType int, poolID int64, volumeConfig map[string]string) (int64, error) {
+func dbStoragePoolVolumeCreate(db *sql.DB, volumeName, volumeDescription string, volumeType int, poolID int64, volumeConfig map[string]string) (int64, error) {
 	tx, err := dbBegin(db)
 	if err != nil {
 		return -1, err
 	}
 
-	result, err := tx.Exec("INSERT INTO storage_volumes (storage_pool_id, type, name) VALUES (?, ?, ?)", poolID, volumeType, volumeName)
+	result, err := tx.Exec("INSERT INTO storage_volumes (storage_pool_id, type, name, description) VALUES (?, ?, ?, ?)",
+		poolID, volumeType, volumeName, volumeDescription)
 	if err != nil {
 		tx.Rollback()
 		return -1, err
diff --git a/lxd/db_storage_volumes.go b/lxd/db_storage_volumes.go
index 738f387..9cce35f 100644
--- a/lxd/db_storage_volumes.go
+++ b/lxd/db_storage_volumes.go
@@ -30,6 +30,29 @@ func dbStorageVolumeConfigGet(db *sql.DB, volumeID int64) (map[string]string, er
 	return config, nil
 }
 
+// Get the description of a storage volume.
+func dbStorageVolumeDescriptionGet(db *sql.DB, volumeID int64) (string, error) {
+	description := sql.NullString{}
+	query := "SELECT description FROM storage_volumes WHERE id=?"
+	inargs := []interface{}{volumeID}
+	outargs := []interface{}{&description}
+
+	err := dbQueryRowScan(db, query, inargs, outargs)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return "", NoSuchObjectError
+		}
+	}
+
+	return description.String, nil
+}
+
+// Update description of a storage volume.
+func dbStorageVolumeDescriptionUpdate(tx *sql.Tx, volumeID int64, description string) error {
+	_, err := tx.Exec("UPDATE storage_volumes SET description=? WHERE id=?", description, volumeID)
+	return err
+}
+
 // Add new storage volume config into database.
 func dbStorageVolumeConfigAdd(tx *sql.Tx, volumeID int64, volumeConfig map[string]string) error {
 	str := "INSERT INTO storage_volumes_config (storage_volume_id, key, value) VALUES(?, ?, ?)"
diff --git a/lxd/db_update.go b/lxd/db_update.go
index 30fc59b..e68e1ea 100644
--- a/lxd/db_update.go
+++ b/lxd/db_update.go
@@ -72,6 +72,7 @@ var dbUpdates = []dbUpdate{
 	{version: 36, run: dbUpdateFromV35},
 	{version: 37, run: dbUpdateFromV36},
 	{version: 38, run: dbUpdateFromV37},
+	{version: 39, run: dbUpdateFromV38},
 }
 
 type dbUpdate struct {
@@ -128,6 +129,11 @@ func dbUpdatesApplyAll(d *Daemon) error {
 }
 
 // Schema updates begin here
+func dbUpdateFromV38(currentVersion int, version int, d *Daemon) error {
+	_, err := d.db.Exec("ALTER TABLE storage_volumes ADD COLUMN description TEXT;")
+	return err
+}
+
 func dbUpdateFromV37(currentVersion int, version int, d *Daemon) error {
 	_, err := d.db.Exec("ALTER TABLE storage_pools ADD COLUMN description TEXT;")
 	return err
diff --git a/lxd/patches.go b/lxd/patches.go
index 837159c..b80f430 100644
--- a/lxd/patches.go
+++ b/lxd/patches.go
@@ -364,13 +364,13 @@ func upgradeFromStorageTypeBtrfs(name string, d *Daemon, defaultPoolName string,
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, ct, storagePoolVolumeTypeContainer, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the container.")
-			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, "", containerPoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, ct, "", storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for container \"%s\".", ct)
 				return err
@@ -452,13 +452,13 @@ func upgradeFromStorageTypeBtrfs(name string, d *Daemon, defaultPoolName string,
 			_, err = dbStoragePoolVolumeGetTypeID(d.db, cs, storagePoolVolumeTypeContainer, poolID)
 			if err == nil {
 				logger.Warnf("Storage volumes database already contains an entry for the snapshot.")
-				err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+				err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, "", snapshotPoolVolumeConfig)
 				if err != nil {
 					return err
 				}
 			} else if err == NoSuchObjectError {
 				// Insert storage volumes for containers into the database.
-				_, err := dbStoragePoolVolumeCreate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+				_, err := dbStoragePoolVolumeCreate(d.db, cs, "", storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
 				if err != nil {
 					logger.Errorf("Could not insert a storage volume for snapshot \"%s\".", cs)
 					return err
@@ -533,13 +533,13 @@ func upgradeFromStorageTypeBtrfs(name string, d *Daemon, defaultPoolName string,
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, img, storagePoolVolumeTypeImage, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the image.")
-			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, "", imagePoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, img, "", storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for image \"%s\".", img)
 				return err
@@ -651,13 +651,13 @@ func upgradeFromStorageTypeDir(name string, d *Daemon, defaultPoolName string, d
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, ct, storagePoolVolumeTypeContainer, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the container.")
-			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, "", containerPoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, ct, "", storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for container \"%s\".", ct)
 				return err
@@ -768,13 +768,13 @@ func upgradeFromStorageTypeDir(name string, d *Daemon, defaultPoolName string, d
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, cs, storagePoolVolumeTypeContainer, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the snapshot.")
-			err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, "", snapshotPoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, cs, "", storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for snapshot \"%s\".", cs)
 				return err
@@ -798,13 +798,13 @@ func upgradeFromStorageTypeDir(name string, d *Daemon, defaultPoolName string, d
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, img, storagePoolVolumeTypeImage, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the image.")
-			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, "", imagePoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, img, "", storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for image \"%s\".", img)
 				return err
@@ -954,13 +954,13 @@ func upgradeFromStorageTypeLvm(name string, d *Daemon, defaultPoolName string, d
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, ct, storagePoolVolumeTypeContainer, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the container.")
-			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, "", containerPoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, ct, "", storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for container \"%s\".", ct)
 				return err
@@ -1109,13 +1109,13 @@ func upgradeFromStorageTypeLvm(name string, d *Daemon, defaultPoolName string, d
 			_, err = dbStoragePoolVolumeGetTypeID(d.db, cs, storagePoolVolumeTypeContainer, poolID)
 			if err == nil {
 				logger.Warnf("Storage volumes database already contains an entry for the snapshot.")
-				err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+				err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, "", snapshotPoolVolumeConfig)
 				if err != nil {
 					return err
 				}
 			} else if err == NoSuchObjectError {
 				// Insert storage volumes for containers into the database.
-				_, err := dbStoragePoolVolumeCreate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+				_, err := dbStoragePoolVolumeCreate(d.db, cs, "", storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
 				if err != nil {
 					logger.Errorf("Could not insert a storage volume for snapshot \"%s\".", cs)
 					return err
@@ -1280,13 +1280,13 @@ func upgradeFromStorageTypeLvm(name string, d *Daemon, defaultPoolName string, d
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, img, storagePoolVolumeTypeImage, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the image.")
-			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, "", imagePoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, img, "", storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for image \"%s\".", img)
 				return err
@@ -1471,13 +1471,13 @@ func upgradeFromStorageTypeZfs(name string, d *Daemon, defaultPoolName string, d
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, ct, storagePoolVolumeTypeContainer, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the container.")
-			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, ct, storagePoolVolumeTypeContainer, poolID, "", containerPoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, ct, storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, ct, "", storagePoolVolumeTypeContainer, poolID, containerPoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for container \"%s\".", ct)
 				return err
@@ -1557,13 +1557,13 @@ func upgradeFromStorageTypeZfs(name string, d *Daemon, defaultPoolName string, d
 			_, err = dbStoragePoolVolumeGetTypeID(d.db, cs, storagePoolVolumeTypeContainer, poolID)
 			if err == nil {
 				logger.Warnf("Storage volumes database already contains an entry for the snapshot.")
-				err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+				err := dbStoragePoolVolumeUpdate(d.db, cs, storagePoolVolumeTypeContainer, poolID, "", snapshotPoolVolumeConfig)
 				if err != nil {
 					return err
 				}
 			} else if err == NoSuchObjectError {
 				// Insert storage volumes for containers into the database.
-				_, err := dbStoragePoolVolumeCreate(d.db, cs, storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
+				_, err := dbStoragePoolVolumeCreate(d.db, cs, "", storagePoolVolumeTypeContainer, poolID, snapshotPoolVolumeConfig)
 				if err != nil {
 					logger.Errorf("Could not insert a storage volume for snapshot \"%s\".", cs)
 					return err
@@ -1613,13 +1613,13 @@ func upgradeFromStorageTypeZfs(name string, d *Daemon, defaultPoolName string, d
 		_, err = dbStoragePoolVolumeGetTypeID(d.db, img, storagePoolVolumeTypeImage, poolID)
 		if err == nil {
 			logger.Warnf("Storage volumes database already contains an entry for the image.")
-			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			err := dbStoragePoolVolumeUpdate(d.db, img, storagePoolVolumeTypeImage, poolID, "", imagePoolVolumeConfig)
 			if err != nil {
 				return err
 			}
 		} else if err == NoSuchObjectError {
 			// Insert storage volumes for containers into the database.
-			_, err := dbStoragePoolVolumeCreate(d.db, img, storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
+			_, err := dbStoragePoolVolumeCreate(d.db, img, "", storagePoolVolumeTypeImage, poolID, imagePoolVolumeConfig)
 			if err != nil {
 				logger.Errorf("Could not insert a storage volume for image \"%s\".", img)
 				return err
@@ -2078,7 +2078,7 @@ func patchStorageApiUpdateStorageConfigs(name string, d *Daemon) error {
 			// exist in the db, so it's safe to ignore the error.
 			volumeType, _ := storagePoolVolumeTypeNameToType(volume.Type)
 			// Update the volume config.
-			err = dbStoragePoolVolumeUpdate(d.db, volume.Name, volumeType, poolID, volume.Config)
+			err = dbStoragePoolVolumeUpdate(d.db, volume.Name, volumeType, poolID, volume.Description, volume.Config)
 			if err != nil {
 				return err
 			}
@@ -2226,7 +2226,7 @@ func patchStorageApiDetectLVSize(name string, d *Daemon) error {
 			// exist in the db, so it's safe to ignore the error.
 			volumeType, _ := storagePoolVolumeTypeNameToType(volume.Type)
 			// Update the volume config.
-			err = dbStoragePoolVolumeUpdate(d.db, volume.Name, volumeType, poolID, volume.Config)
+			err = dbStoragePoolVolumeUpdate(d.db, volume.Name, volumeType, poolID, volume.Description, volume.Config)
 			if err != nil {
 				return err
 			}
diff --git a/lxd/storage_shared.go b/lxd/storage_shared.go
index d6378e5..fd5f1c6 100644
--- a/lxd/storage_shared.go
+++ b/lxd/storage_shared.go
@@ -106,7 +106,7 @@ func (s *storageShared) createImageDbPoolVolume(fingerprint string) error {
 	}
 
 	// Create a db entry for the storage volume of the image.
-	_, err = dbStoragePoolVolumeCreate(s.d.db, fingerprint, storagePoolVolumeTypeImage, s.poolID, volumeConfig)
+	_, err = dbStoragePoolVolumeCreate(s.d.db, fingerprint, "", storagePoolVolumeTypeImage, s.poolID, volumeConfig)
 	if err != nil {
 		// Try to delete the db entry on error.
 		s.deleteImageDbPoolVolume(fingerprint)
diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go
index a0d09f9..dcb7a64 100644
--- a/lxd/storage_volumes.go
+++ b/lxd/storage_volumes.go
@@ -162,7 +162,7 @@ func storagePoolVolumesTypePost(d *Daemon, r *http.Request) Response {
 	// volume is supposed to be created.
 	poolName := mux.Vars(r)["name"]
 
-	err = storagePoolVolumeCreateInternal(d, poolName, req.Name, req.Type, req.Config)
+	err = storagePoolVolumeCreateInternal(d, poolName, req.Name, req.Description, req.Type, req.Config)
 	if err != nil {
 		return InternalError(err)
 	}
@@ -276,7 +276,7 @@ func storagePoolVolumeTypePut(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 
-	err = storagePoolVolumeUpdate(d, poolName, req.Name, volumeType, req.Config)
+	err = storagePoolVolumeUpdate(d, poolName, req.Name, volumeType, req.Description, req.Config)
 	if err != nil {
 		return InternalError(err)
 	}
@@ -349,7 +349,7 @@ func storagePoolVolumeTypePatch(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 
-	err = storagePoolVolumeUpdate(d, poolName, req.Name, volumeType, req.Config)
+	err = storagePoolVolumeUpdate(d, poolName, req.Name, volumeType, req.Description, req.Config)
 	if err != nil {
 		return InternalError(err)
 	}
diff --git a/lxd/storage_volumes_utils.go b/lxd/storage_volumes_utils.go
index a216e36..0f03958 100644
--- a/lxd/storage_volumes_utils.go
+++ b/lxd/storage_volumes_utils.go
@@ -88,7 +88,7 @@ func storagePoolVolumeTypeToAPIEndpoint(volumeType int) (string, error) {
 	return "", fmt.Errorf("invalid storage volume type")
 }
 
-func storagePoolVolumeUpdate(d *Daemon, poolName string, volumeName string, volumeType int, newConfig map[string]string) error {
+func storagePoolVolumeUpdate(d *Daemon, poolName string, volumeName string, volumeType int, newDescription string, newConfig map[string]string) error {
 	s, err := storagePoolVolumeInit(d, poolName, volumeName, volumeType)
 	if err != nil {
 		return err
@@ -98,6 +98,7 @@ func storagePoolVolumeUpdate(d *Daemon, poolName string, volumeName string, volu
 	newWritable := oldWritable
 
 	// Backup the current state
+	oldDescription := oldWritable.Description
 	oldConfig := map[string]string{}
 	err = shared.DeepCopy(&oldWritable.Config, &oldConfig)
 	if err != nil {
@@ -142,33 +143,34 @@ func storagePoolVolumeUpdate(d *Daemon, poolName string, volumeName string, volu
 		}
 	}
 
-	// Skip on no change
-	if len(changedConfig) == 0 {
-		return nil
-	}
+	// Apply config changes if there are any
+	if len(changedConfig) != 0 {
 
-	// Update the storage pool
-	if !userOnly {
-		err = s.StoragePoolVolumeUpdate(changedConfig)
-		if err != nil {
-			return err
+		// Update the storage pool
+		if !userOnly {
+			err = s.StoragePoolVolumeUpdate(changedConfig)
+			if err != nil {
+				return err
+			}
 		}
-	}
 
-	newWritable.Config = newConfig
+		newWritable.Config = newConfig
 
-	// Apply the new configuration
-	s.SetStoragePoolVolumeWritable(&newWritable)
+		// Apply the new configuration
+		s.SetStoragePoolVolumeWritable(&newWritable)
+	}
 
 	poolID, err := dbStoragePoolGetID(d.db, poolName)
 	if err != nil {
 		return err
 	}
 
-	// Update the database
-	err = dbStoragePoolVolumeUpdate(d.db, volumeName, volumeType, poolID, newConfig)
-	if err != nil {
-		return err
+	// Update the database if something changed
+	if len(changedConfig) != 0 || newDescription != oldDescription {
+		err = dbStoragePoolVolumeUpdate(d.db, volumeName, volumeType, poolID, newDescription, newConfig)
+		if err != nil {
+			return err
+		}
 	}
 
 	// Success, update the closure to mark that the changes should be kept.
@@ -260,7 +262,7 @@ func profilesUsingPoolVolumeGetNames(db *sql.DB, volumeName string, volumeType s
 	return usedBy, nil
 }
 
-func storagePoolVolumeDBCreate(d *Daemon, poolName string, volumeName string, volumeTypeName string, volumeConfig map[string]string) error {
+func storagePoolVolumeDBCreate(d *Daemon, poolName string, volumeName, volumeDescription string, volumeTypeName string, volumeConfig map[string]string) error {
 	// Check that the name of the new storage volume is valid. (For example.
 	// zfs pools cannot contain "/" in their names.)
 	err := storageValidName(volumeName)
@@ -311,7 +313,7 @@ func storagePoolVolumeDBCreate(d *Daemon, poolName string, volumeName string, vo
 	}
 
 	// Create the database entry for the storage volume.
-	_, err = dbStoragePoolVolumeCreate(d.db, volumeName, volumeType, poolID, volumeConfig)
+	_, err = dbStoragePoolVolumeCreate(d.db, volumeName, volumeDescription, volumeType, poolID, volumeConfig)
 	if err != nil {
 		return fmt.Errorf("Error inserting %s of type %s into database: %s", poolName, volumeTypeName, err)
 	}
@@ -319,8 +321,8 @@ func storagePoolVolumeDBCreate(d *Daemon, poolName string, volumeName string, vo
 	return nil
 }
 
-func storagePoolVolumeCreateInternal(d *Daemon, poolName string, volumeName string, volumeTypeName string, volumeConfig map[string]string) error {
-	err := storagePoolVolumeDBCreate(d, poolName, volumeName, volumeTypeName, volumeConfig)
+func storagePoolVolumeCreateInternal(d *Daemon, poolName string, volumeName, volumeDescription string, volumeTypeName string, volumeConfig map[string]string) error {
+	err := storagePoolVolumeDBCreate(d, poolName, volumeName, volumeDescription, volumeTypeName, volumeConfig)
 	if err != nil {
 		return err
 	}
diff --git a/shared/api/storage.go b/shared/api/storage.go
index 297b28e..28b1d32 100644
--- a/shared/api/storage.go
+++ b/shared/api/storage.go
@@ -44,7 +44,6 @@ type StorageVolumesPost struct {
 // API extension: storage
 type StorageVolume struct {
 	StorageVolumePut `yaml:",inline"`
-
 	Name   string   `json:"name" yaml:"name"`
 	Type   string   `json:"type" yaml:"type"`
 	UsedBy []string `json:"used_by" yaml:"used_by"`
@@ -54,7 +53,8 @@ type StorageVolume struct {
 //
 // API extension: storage
 type StorageVolumePut struct {
-	Config map[string]string `json:"config" yaml:"config"`
+	Description string            `json:"description" yaml:"description"`
+	Config      map[string]string `json:"config" yaml:"config"`
 }
 
 // Writable converts a full StoragePool struct into a StoragePoolPut struct
diff --git a/test/suites/storage.sh b/test/suites/storage.sh
index 44eb939..3008c5f 100644
--- a/test/suites/storage.sh
+++ b/test/suites/storage.sh
@@ -7,16 +7,21 @@ test_storage() {
   chmod +x "${LXD_STORAGE_DIR}"
   spawn_lxd "${LXD_STORAGE_DIR}" false
 
-  # edit storage description
-
+  # edit storage and pool description
   # shellcheck disable=2039
-  local storage_pool
+  local storage_pool storage_volume
   storage_pool="lxdtest-$(basename "${LXD_DIR}")-pool"
+  storage_volume="${storage_pool}-vol"
   lxc storage create "$storage_pool" "$lxd_backend"
   lxc storage show "$storage_pool" | sed 's/^description:.*/description: foo/' | lxc storage edit "$storage_pool"
   lxc storage show "$storage_pool" | grep -q 'description: foo'
-  lxc storage delete "$storage_pool"
 
+  lxc storage volume create "$storage_pool" "$storage_volume"
+  lxc storage volume show "$storage_pool" "$storage_volume" | sed 's/^description:.*/description: bar/' | lxc storage volume edit "$storage_pool" "$storage_volume"
+  lxc storage volume show "$storage_pool" "$storage_volume" | grep -q 'description: bar'
+  lxc storage volume delete "$storage_pool" "$storage_volume"
+
+  lxc storage delete "$storage_pool"
   (
     set -e
     # shellcheck disable=2030

From 6959aac05dbca3444a16a28b88f763a91150b1d4 Mon Sep 17 00:00:00 2001
From: Alberto Donato <alberto.donato at canonical.com>
Date: Tue, 2 May 2017 13:43:50 +0200
Subject: [PATCH 5/6] Add description field to containers.

Signed-off-by: Alberto Donato <alberto.donato at canonical.com>
---
 lxc/list.go             |  9 ++++++++-
 lxc/list_test.go        |  2 +-
 lxd/container.go        |  2 ++
 lxd/container_lxc.go    | 13 ++++++++++++-
 lxd/container_patch.go  |  1 +
 lxd/container_put.go    |  1 +
 lxd/container_state.go  |  1 +
 lxd/db.go               |  1 +
 lxd/db_containers.go    | 13 ++++++++-----
 lxd/db_update.go        |  6 ++++++
 shared/api/container.go |  1 +
 shared/api/storage.go   |  6 +++---
 test/main.sh            |  2 ++
 test/suites/config.sh   | 31 +++++++++++++++++++++++++++++++
 14 files changed, 78 insertions(+), 11 deletions(-)

diff --git a/lxc/list.go b/lxc/list.go
index 491264a..387e71d 100644
--- a/lxc/list.go
+++ b/lxc/list.go
@@ -92,6 +92,8 @@ Pre-defined column shorthand chars:
 
     c - Creation date
 
+    d - Description
+
     l - Last used date
 
     n - Name
@@ -129,7 +131,7 @@ lxc list -c ns,user.comment:comment
 
 func (c *listCmd) flags() {
 	gnuflag.StringVar(&c.columnsRaw, "c", "ns46tS", i18n.G("Columns"))
-	gnuflag.StringVar(&c.columnsRaw, "columns", "ns46tS", i18n.G("Columns"))
+	gnuflag.StringVar(&c.columnsRaw, "columns", "nds46tS", i18n.G("Columns"))
 	gnuflag.StringVar(&c.format, "format", "table", i18n.G("Format (table|json|csv)"))
 	gnuflag.BoolVar(&c.fast, "fast", false, i18n.G("Fast mode (same as --columns=nsacPt)"))
 }
@@ -445,6 +447,7 @@ func (c *listCmd) parseColumns() ([]column, error) {
 		'6': {i18n.G("IPV6"), c.IP6ColumnData, true, false},
 		'a': {i18n.G("ARCHITECTURE"), c.ArchitectureColumnData, false, false},
 		'c': {i18n.G("CREATED AT"), c.CreatedColumnData, false, false},
+		'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData, false, false},
 		'l': {i18n.G("LAST USED AT"), c.LastUsedColumnData, false, false},
 		'n': {i18n.G("NAME"), c.nameColumnData, false, false},
 		'p': {i18n.G("PID"), c.PIDColumnData, true, false},
@@ -535,6 +538,10 @@ func (c *listCmd) nameColumnData(cInfo api.Container, cState *api.ContainerState
 	return cInfo.Name
 }
 
+func (c *listCmd) descriptionColumnData(cInfo api.Container, cState *api.ContainerState, cSnaps []api.ContainerSnapshot) string {
+	return cInfo.Description
+}
+
 func (c *listCmd) statusColumnData(cInfo api.Container, cState *api.ContainerState, cSnaps []api.ContainerSnapshot) string {
 	return strings.ToUpper(cInfo.Status)
 }
diff --git a/lxc/list_test.go b/lxc/list_test.go
index 8364fa1..6bdf62e 100644
--- a/lxc/list_test.go
+++ b/lxc/list_test.go
@@ -52,7 +52,7 @@ func TestShouldShow(t *testing.T) {
 }
 
 // Used by TestColumns and TestInvalidColumns
-const shorthand = "46abclnpPsSt"
+const shorthand = "46abcdlnpPsSt"
 const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
 
 func TestColumns(t *testing.T) {
diff --git a/lxd/container.go b/lxd/container.go
index c448762..a5c8624 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -401,6 +401,7 @@ type containerArgs struct {
 	// Don't set manually
 	Id int
 
+	Description  string
 	Architecture int
 	BaseImage    string
 	Config       map[string]string
@@ -481,6 +482,7 @@ type container interface {
 	// Properties
 	Id() int
 	Name() string
+	Description() string
 	Architecture() int
 	CreationDate() time.Time
 	LastUsedDate() time.Time
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index ac362e3..96ac8eb 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -187,6 +187,7 @@ func containerLXCCreate(d *Daemon, args containerArgs) (container, error) {
 		daemon:       d,
 		id:           args.Id,
 		name:         args.Name,
+		description:  args.Description,
 		ephemeral:    args.Ephemeral,
 		architecture: args.Architecture,
 		cType:        args.Ctype,
@@ -351,6 +352,7 @@ func containerLXCLoad(d *Daemon, args containerArgs) (container, error) {
 		daemon:       d,
 		id:           args.Id,
 		name:         args.Name,
+		description:  args.Description,
 		ephemeral:    args.Ephemeral,
 		architecture: args.Architecture,
 		cType:        args.Ctype,
@@ -381,6 +383,7 @@ type containerLXC struct {
 	ephemeral    bool
 	id           int
 	name         string
+	description  string
 	stateful     bool
 
 	// Config
@@ -2512,6 +2515,7 @@ func (c *containerLXC) Render() (interface{}, interface{}, error) {
 			StatusCode:      statusCode,
 		}
 
+		ct.Description = c.Description()
 		ct.Architecture = architectureName
 		ct.Config = c.localConfig
 		ct.CreatedAt = c.creationDate
@@ -3124,6 +3128,7 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error {
 	}
 
 	// Get a copy of the old configuration
+	oldDescription := c.Description()
 	oldArchitecture := 0
 	err = shared.DeepCopy(&c.architecture, &oldArchitecture)
 	if err != nil {
@@ -3173,6 +3178,7 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error {
 	undoChanges := true
 	defer func() {
 		if undoChanges {
+			c.description = oldDescription
 			c.architecture = oldArchitecture
 			c.ephemeral = oldEphemeral
 			c.expandedConfig = oldExpandedConfig
@@ -3187,6 +3193,7 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error {
 	}()
 
 	// Apply the various changes
+	c.description = args.Description
 	c.architecture = args.Architecture
 	c.ephemeral = args.Ephemeral
 	c.localConfig = args.Config
@@ -3887,7 +3894,7 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error {
 		return err
 	}
 
-	err = dbContainerUpdate(tx, c.id, c.architecture, c.ephemeral)
+	err = dbContainerUpdate(tx, c.id, c.description, c.architecture, c.ephemeral)
 	if err != nil {
 		tx.Rollback()
 		return err
@@ -6745,6 +6752,10 @@ func (c *containerLXC) Name() string {
 	return c.name
 }
 
+func (c *containerLXC) Description() string {
+	return c.description
+}
+
 func (c *containerLXC) Profiles() []string {
 	return c.profiles
 }
diff --git a/lxd/container_patch.go b/lxd/container_patch.go
index f650c91..0d86bd1 100644
--- a/lxd/container_patch.go
+++ b/lxd/container_patch.go
@@ -101,6 +101,7 @@ func containerPatch(d *Daemon, r *http.Request) Response {
 	// Update container configuration
 	args := containerArgs{
 		Architecture: architecture,
+		Description:  req.Description,
 		Config:       req.Config,
 		Devices:      req.Devices,
 		Ephemeral:    req.Ephemeral,
diff --git a/lxd/container_put.go b/lxd/container_put.go
index 14640c2..29ae21d 100644
--- a/lxd/container_put.go
+++ b/lxd/container_put.go
@@ -52,6 +52,7 @@ func containerPut(d *Daemon, r *http.Request) Response {
 		do = func(op *operation) error {
 			args := containerArgs{
 				Architecture: architecture,
+				Description:  configRaw.Description,
 				Config:       configRaw.Config,
 				Devices:      configRaw.Devices,
 				Ephemeral:    configRaw.Ephemeral,
diff --git a/lxd/container_state.go b/lxd/container_state.go
index 3115f58..a766db8 100644
--- a/lxd/container_state.go
+++ b/lxd/container_state.go
@@ -100,6 +100,7 @@ func containerStatePut(d *Daemon, r *http.Request) Response {
 			if ephemeral {
 				// Unset ephemeral flag
 				args := containerArgs{
+					Description:  c.Description(),
 					Architecture: c.Architecture(),
 					Config:       c.LocalConfig(),
 					Devices:      c.LocalDevices(),
diff --git a/lxd/db.go b/lxd/db.go
index b51497a..02cab4c 100644
--- a/lxd/db.go
+++ b/lxd/db.go
@@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS config (
 CREATE TABLE IF NOT EXISTS containers (
     id INTEGER primary key AUTOINCREMENT NOT NULL,
     name VARCHAR(255) NOT NULL,
+    description TEXT,
     architecture INTEGER NOT NULL,
     type INTEGER NOT NULL,
     ephemeral INTEGER NOT NULL DEFAULT 0,
diff --git a/lxd/db_containers.go b/lxd/db_containers.go
index 58255da..87cc988 100644
--- a/lxd/db_containers.go
+++ b/lxd/db_containers.go
@@ -53,20 +53,23 @@ func dbContainerId(db *sql.DB, name string) (int, error) {
 
 func dbContainerGet(db *sql.DB, name string) (containerArgs, error) {
 	var used *time.Time // Hold the db-returned time
+	description := sql.NullString{}
 
 	args := containerArgs{}
 	args.Name = name
 
 	ephemInt := -1
 	statefulInt := -1
-	q := "SELECT id, architecture, type, ephemeral, stateful, creation_date, last_use_date FROM containers WHERE name=?"
+	q := "SELECT id, description, architecture, type, ephemeral, stateful, creation_date, last_use_date FROM containers WHERE name=?"
 	arg1 := []interface{}{name}
-	arg2 := []interface{}{&args.Id, &args.Architecture, &args.Ctype, &ephemInt, &statefulInt, &args.CreationDate, &used}
+	arg2 := []interface{}{&args.Id, &description, &args.Architecture, &args.Ctype, &ephemInt, &statefulInt, &args.CreationDate, &used}
 	err := dbQueryRowScan(db, q, arg1, arg2)
 	if err != nil {
 		return args, err
 	}
 
+	args.Description = description.String
+
 	if args.Id == -1 {
 		return args, fmt.Errorf("Unknown container")
 	}
@@ -396,8 +399,8 @@ func dbContainerRename(db *sql.DB, oldName string, newName string) error {
 	return txCommit(tx)
 }
 
-func dbContainerUpdate(tx *sql.Tx, id int, architecture int, ephemeral bool) error {
-	str := fmt.Sprintf("UPDATE containers SET architecture=?, ephemeral=? WHERE id=?")
+func dbContainerUpdate(tx *sql.Tx, id int, description string, architecture int, ephemeral bool) error {
+	str := fmt.Sprintf("UPDATE containers SET description=?, architecture=?, ephemeral=? WHERE id=?")
 	stmt, err := tx.Prepare(str)
 	if err != nil {
 		return err
@@ -409,7 +412,7 @@ func dbContainerUpdate(tx *sql.Tx, id int, architecture int, ephemeral bool) err
 		ephemeralInt = 1
 	}
 
-	if _, err := stmt.Exec(architecture, ephemeralInt, id); err != nil {
+	if _, err := stmt.Exec(description, architecture, ephemeralInt, id); err != nil {
 		return err
 	}
 
diff --git a/lxd/db_update.go b/lxd/db_update.go
index e68e1ea..20e6ce8 100644
--- a/lxd/db_update.go
+++ b/lxd/db_update.go
@@ -73,6 +73,7 @@ var dbUpdates = []dbUpdate{
 	{version: 37, run: dbUpdateFromV36},
 	{version: 38, run: dbUpdateFromV37},
 	{version: 39, run: dbUpdateFromV38},
+	{version: 40, run: dbUpdateFromV39},
 }
 
 type dbUpdate struct {
@@ -129,6 +130,11 @@ func dbUpdatesApplyAll(d *Daemon) error {
 }
 
 // Schema updates begin here
+func dbUpdateFromV39(currentVersion int, version int, d *Daemon) error {
+	_, err := d.db.Exec("ALTER TABLE containers ADD COLUMN description TEXT;")
+	return err
+}
+
 func dbUpdateFromV38(currentVersion int, version int, d *Daemon) error {
 	_, err := d.db.Exec("ALTER TABLE storage_volumes ADD COLUMN description TEXT;")
 	return err
diff --git a/shared/api/container.go b/shared/api/container.go
index 849b9bf..ec6ff4d 100644
--- a/shared/api/container.go
+++ b/shared/api/container.go
@@ -29,6 +29,7 @@ type ContainerPost struct {
 
 // ContainerPut represents the modifiable fields of a LXD container
 type ContainerPut struct {
+	Description  string                       `json:"description" yaml:"description"`
 	Architecture string                       `json:"architecture" yaml:"architecture"`
 	Config       map[string]string            `json:"config" yaml:"config"`
 	Devices      map[string]map[string]string `json:"devices" yaml:"devices"`
diff --git a/shared/api/storage.go b/shared/api/storage.go
index 28b1d32..bfe3d08 100644
--- a/shared/api/storage.go
+++ b/shared/api/storage.go
@@ -44,9 +44,9 @@ type StorageVolumesPost struct {
 // API extension: storage
 type StorageVolume struct {
 	StorageVolumePut `yaml:",inline"`
-	Name   string   `json:"name" yaml:"name"`
-	Type   string   `json:"type" yaml:"type"`
-	UsedBy []string `json:"used_by" yaml:"used_by"`
+	Name             string   `json:"name" yaml:"name"`
+	Type             string   `json:"type" yaml:"type"`
+	UsedBy           []string `json:"used_by" yaml:"used_by"`
 }
 
 // StorageVolumePut represents the modifiable fields of a LXD storage volume.
diff --git a/test/main.sh b/test/main.sh
index d470070..5728cb7 100755
--- a/test/main.sh
+++ b/test/main.sh
@@ -611,6 +611,8 @@ run_test test_concurrent "concurrent startup"
 run_test test_snapshots "container snapshots"
 run_test test_snap_restore "snapshot restores"
 run_test test_config_profiles "profiles and configuration"
+run_test test_config_edit "container configuration edit"
+run_test test_config_edit_container_snapshot_pool_config "container and snapshot volume configuration edit"
 run_test test_server_config "server configuration"
 run_test test_filemanip "file manipulations"
 run_test test_network "network management"
diff --git a/test/suites/config.sh b/test/suites/config.sh
index d3e9a19..01cb22e 100644
--- a/test/suites/config.sh
+++ b/test/suites/config.sh
@@ -225,3 +225,34 @@ test_config_profiles() {
   lxc stop foo --force
   lxc delete foo
 }
+
+
+test_config_edit() {
+    ensure_import_testimage
+
+    lxc init testimage foo -s "lxdtest-$(basename "${LXD_DIR}")"
+    lxc config show foo | sed 's/^description:.*/description: bar/' | lxc config edit foo
+    lxc config show foo | grep -q 'description: bar'
+    lxc delete foo
+}
+
+test_config_edit_container_snapshot_pool_config() {
+    # shellcheck disable=2034,2039,2155
+    local storage_pool="lxdtest-$(basename "${LXD_DIR}")"
+
+    ensure_import_testimage
+
+    lxc init testimage c1 -s "$storage_pool"
+    lxc snapshot c1 s1
+    # edit the container volume name
+    lxc storage volume show "$storage_pool" container/c1 | \
+        sed 's/^description:.*/description: bar/' | \
+        lxc storage volume edit "$storage_pool" container/c1
+    lxc storage volume show "$storage_pool" container/c1 | grep -q 'description: bar'
+    # edit the container snapshot volume name
+    lxc storage volume show "$storage_pool" container/c1/s1 | \
+        sed 's/^description:.*/description: baz/' | \
+        lxc storage volume edit "$storage_pool" container/c1/s1
+    lxc storage volume show "$storage_pool" container/c1/s1 | grep -q 'description: baz'
+    lxc delete c1
+}

From 69cb2b619c5177de0f9ed9070adfe058147ce416 Mon Sep 17 00:00:00 2001
From: Alberto Donato <alberto.donato at canonical.com>
Date: Wed, 3 May 2017 11:58:13 +0200
Subject: [PATCH 6/6] Add entity_description extension.

Signed-off-by: Alberto Donato <alberto.donato at canonical.com>
---
 doc/api-extensions.md | 3 +++
 lxd/api_1.0.go        | 1 +
 2 files changed, 4 insertions(+)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index ee37780..4c81df2 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -269,3 +269,6 @@ This key control what host network interface is used for a VXLAN tunnel.
 This introduces the btrfs.mount\_options property for btrfs storage pools.
 
 This key controls what mount options will be used for the btrfs storage pool.
+
+## entity\_description
+This adds descriptions to entities like containers, snapshots, networks, storage pools and volumes.
diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index beb4f79..31ed765 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -105,6 +105,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
 			"storage_rsync_bwlimit",
 			"network_vxlan_interface",
 			"storage_btrfs_mount_options",
+			"entity_description",
 		},
 		APIStatus:  "stable",
 		APIVersion: version.APIVersion,


More information about the lxc-devel mailing list