public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui
@ 2022-11-14  9:43 Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
                   ` (20 more replies)
  0 siblings, 21 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

this series brings the already existing 'tags' for ct/vms to the gui:
* tags can be edited in the status toolbar of the guest
* existing tags will be shown in the tree/global search/resource grids
* when editing a tag, a list of tags will be shown
* by default, the color is (consistently) autogenerated based on the
  text
* that color can be overriden in datacenter -> options (cluster wide)
  (edit window for browser local storage is TBD)
* by default, the text color is either white or black, depending which
  provides the greater contrast (according to SAPC)
* this text color can also be overridden
* there are multiple shapes available for the tree
* implements some permission control in datacenter.cfg with
  'user-tag-access' and 'privileged-tags' with that it's possible to have
  better control over what tags a user can actually add to its guests
  i intentionally left out the gui for those for now, but they shouldn't
  be that hard to add, should we go this way

some notes:
* noticed that on firefox in linux, something is off with the
  font-rendering, the text is weirdly aligned. not only with my tags,
  but also in the regular buttons, etc. but with the editing of the
  tags it's noticable. on a windows 10 machine i don't have the problem
  with firefox (and chrome/chromium is ok also on linux)
* the privileged tags/user-tag-access editing is very basic, but imho
  functional. if we want to have it 'fancier' please tell ;)
* maybe having the allowed tags in the 'ui-options' is not the
  completely right place, but we only use it in the ui, and i did not
  want to add another api call we have to call after logging in
  (imho it's border-line already with)

changes from v8:
* renamed datacenter.cfg fields according to thomas input
* reworked the 'get_user_admin_tags' function to 'get_allowed_tags'
  which now also checks the appropriate permissions and only returns
  the allowed tags for the given user. (in list context also returns
  the admin tags and if 'freeform' is allowed so that users can
  check the privileges properly). this list is also added to the
  'ui-options' api call, since we'll use it for showing tag suggestions
* included the missing css styles in the appropriate patches
* changed the styling of tags while editings (emulate a textfield) to
  make it clearer it's ready for editing
* changed the color of the 'add tag' field
* added a gui for editing the privileged tags and the user-tag-access
  field
* imroved wording + description
* improved commit messages
* changed how we collect the taglist for the tag picker
* mention the tags in the privilige check
* added the allowed tags to the ui-options

changes from v7:
* rebase on master
* changed admin tags from special syntax to datacenter.cfg option
* add 'user-tag-privleges' option and according api permission checks
* fixed some small bugs with permission checks (e.g. now we check
  the tag count, so that users cannot add tags multiple times that
  already exist but they have no privileges for)
* completely reworked the form/Tag and form/TagEdit, their
  implmementation is now much cleaner imho (squashed the drag&drop
  changes into the intial patches)
* squashed some patches together that fit together
* fixed some drag&drop bugs, so it should now work much better

(omitted older changelog)

Dominik Csapak (4):
  add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  Cluster: add get_guest_config_properties
  datacenter.cfg: add option for tag-style
  datacenter.cfg: add tag rights control to the datacenter config

 data/PVE/Cluster.pm          |  27 ++++++
 data/PVE/DataCenterConfig.pm | 144 ++++++++++++++++++++++++++++
 data/src/cfs-ipc-ops.h       |   2 +
 data/src/server.c            |  64 +++++++++++++
 data/src/status.c            | 177 ++++++++++++++++++++++++-----------
 data/src/status.h            |   3 +
 6 files changed, 361 insertions(+), 56 deletions(-)

-- 
2.30.2

From 4a1d0a9927aa9531cc34aa395519d5142449cfe8 Mon Sep 17 00:00:00 2001
From: Dominik Csapak <d.csapak@proxmox.com>
Date: Mon, 14 Nov 2022 09:57:56 +0100
Subject: [PATCH qemu-server v9 0/1] *** SUBJECT HERE ***

*** BLURB HERE ***

Dominik Csapak (1):
  api: update: improve tag privilege check

 PVE/API2/Qemu.pm | 53 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 52 insertions(+), 1 deletion(-)

-- 
2.30.2

From 8ab86c7a7a176062b78d3027351116bf86b4596c Mon Sep 17 00:00:00 2001
From: Dominik Csapak <d.csapak@proxmox.com>
Date: Mon, 14 Nov 2022 09:58:04 +0100
Subject: [PATCH container v9 0/1] *** SUBJECT HERE ***

*** BLURB HERE ***

Dominik Csapak (1):
  check_ct_modify_config_perm: improve tag privilege check

 src/PVE/LXC.pm | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

-- 
2.30.2

From 96116645c7539c3cf0d2de23f9400fbefd4a2cc9 Mon Sep 17 00:00:00 2001
From: Dominik Csapak <d.csapak@proxmox.com>
Date: Mon, 14 Nov 2022 09:58:12 +0100
Subject: [PATCH widget-toolkit v9 0/2] *** SUBJECT HERE ***

*** BLURB HERE ***

Dominik Csapak (2):
  add tag related helpers
  Toolkit: add override for Ext.dd.DragDropManager

 src/Toolkit.js       | 16 ++++++++
 src/Utils.js         | 88 ++++++++++++++++++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 45 ++++++++++++++++++++++
 3 files changed, 149 insertions(+)

-- 
2.30.2

From 07ea30d3efa2758fc8f026ef1bc3f1ea2b4d63b6 Mon Sep 17 00:00:00 2001
From: Dominik Csapak <d.csapak@proxmox.com>
Date: Mon, 14 Nov 2022 09:58:20 +0100
Subject: [PATCH manager v9 00/12] *** SUBJECT HERE ***

*** BLURB HERE ***

Dominik Csapak (12):
  api: /cluster/resources: add tags to returned properties
  api: add /ui-options api call
  ui: call '/ui-options' and save the result in PVE.UIOptions
  ui: parse and save tag infos from /ui-options
  ui: add form/TagColorGrid
  ui: add PVE.form.ListField
  ui: dc/OptionView: add editors for tag settings
  ui: add form/Tag
  ui: add form/TagEdit.js
  ui: {lxc,qemu}/Config: show Tags and make them editable
  ui: tree/ResourceTree: show Tags in tree
  ui: add tags to ResourceGrid and GlobalSearchField

 PVE/API2.pm                            |  56 ++++
 PVE/API2/Cluster.pm                    |   9 +-
 www/css/ext6-pve.css                   |  52 ++++
 www/manager6/Makefile                  |   4 +
 www/manager6/Utils.js                  |  92 ++++++-
 www/manager6/Workspace.js              |   2 +
 www/manager6/data/ResourceStore.js     |   7 +
 www/manager6/dc/OptionView.js          | 204 +++++++++++++-
 www/manager6/form/GlobalSearchField.js |  20 +-
 www/manager6/form/ListField.js         | 165 ++++++++++++
 www/manager6/form/Tag.js               | 233 ++++++++++++++++
 www/manager6/form/TagColorGrid.js      | 357 +++++++++++++++++++++++++
 www/manager6/form/TagEdit.js           | 321 ++++++++++++++++++++++
 www/manager6/grid/ResourceGrid.js      |   1 +
 www/manager6/lxc/Config.js             |  36 ++-
 www/manager6/qemu/Config.js            |  35 ++-
 www/manager6/tree/ResourceTree.js      |  10 +-
 17 files changed, 1586 insertions(+), 18 deletions(-)
 create mode 100644 www/manager6/form/ListField.js
 create mode 100644 www/manager6/form/Tag.js
 create mode 100644 www/manager6/form/TagColorGrid.js
 create mode 100644 www/manager6/form/TagEdit.js

-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14 13:15   ` Wolfgang Bumiller
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 2/4] Cluster: add get_guest_config_properties Dominik Csapak
                   ` (19 subsequent siblings)
  20 siblings, 1 reply; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

for getting multiple properties from the in memory config of the
guests in one go. Keep the existing IPC call as is for backward
compatibility and add this as separate, new one.

It basically behaves the same as
CFS_IPC_GET_GUEST_CONFIG_PROPERTY, but takes a list of properties
instead and returns multiple properties per guest.

The old way of getting a single property is now also done by
the new function, since it basically performs identically.

Here a short benchmark:

Setup:
PVE in a VM with cpu type host (12700k) and 4 cores
10000 typical configs with both 'lock' and 'tags' set at the end
and fairly long tags ('test-tag1,test-tag2,test-tag3')
(normal vm with a snapshot, ~1KiB)

i let it run 100 times each, times in ms

old code:

total time  time per iteration
1054.2      10.2

new code:

num props  total time  time per iter  function
2          1332.2      13.2           get_properties
1          1051.2      10.2           get_properties
2          2082.2      20.2           get_property (2 calls to get_property)
1          1034.2      10.2           get_property

so a call with the new code for one property is the same as with the
old code, and adding a second property only adds a bit of additional
time (in this case ~30%)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 data/src/cfs-ipc-ops.h |   2 +
 data/src/server.c      |  64 +++++++++++++++
 data/src/status.c      | 177 ++++++++++++++++++++++++++++-------------
 data/src/status.h      |   3 +
 4 files changed, 190 insertions(+), 56 deletions(-)

diff --git a/data/src/cfs-ipc-ops.h b/data/src/cfs-ipc-ops.h
index 003e233..249308d 100644
--- a/data/src/cfs-ipc-ops.h
+++ b/data/src/cfs-ipc-ops.h
@@ -43,4 +43,6 @@
 
 #define CFS_IPC_VERIFY_TOKEN 12
 
+#define CFS_IPC_GET_GUEST_CONFIG_PROPERTIES 13
+
 #endif
diff --git a/data/src/server.c b/data/src/server.c
index 549788a..ced9cfc 100644
--- a/data/src/server.c
+++ b/data/src/server.c
@@ -89,6 +89,13 @@ typedef struct {
 	char property[];
 } cfs_guest_config_propery_get_request_header_t;
 
+typedef struct {
+	struct qb_ipc_request_header req_header;
+	uint32_t vmid;
+	uint8_t num_props;
+	char props[]; /* list of \0 terminated properties */
+} cfs_guest_config_properties_get_request_header_t;
+
 typedef struct {
 	struct qb_ipc_request_header req_header;
 	char token[];
@@ -348,6 +355,63 @@ static int32_t s1_msg_process_fn(
 
 			result = cfs_create_guest_conf_property_msg(outbuf, memdb, rh->property, rh->vmid);
 		}
+	} else if (request_id == CFS_IPC_GET_GUEST_CONFIG_PROPERTIES) {
+
+		cfs_guest_config_properties_get_request_header_t *rh =
+			(cfs_guest_config_properties_get_request_header_t *) data;
+
+		size_t remaining = request_size - G_STRUCT_OFFSET(cfs_guest_config_properties_get_request_header_t, props);
+
+		result = 0;
+		if (rh->vmid < 100 && rh->vmid != 0) {
+			cfs_debug("vmid out of range %u", rh->vmid);
+			result = -EINVAL;
+		} else if (rh->vmid >= 100 && !vmlist_vm_exists(rh->vmid)) {
+			result = -ENOENT;
+		} else if (rh->num_props == 0) {
+			cfs_debug("num_props == 0");
+			result = -EINVAL;
+		} else if (remaining <= 1) {
+			cfs_debug("property length <= 1, %ld", remaining);
+			result = -EINVAL;
+		} else {
+			const char **properties = malloc(sizeof(char*) * rh->num_props);
+			char *current = (rh->props);
+			for (uint8_t i = 0; i < rh->num_props; i++) {
+			    size_t proplen = strnlen(current, remaining);
+			    if (proplen == 0) {
+				cfs_debug("property length 0");
+				result = -EINVAL;
+				break;
+			    }
+			    if (proplen == remaining) {
+				cfs_debug("property not \\0 terminated");
+				result = -EINVAL;
+				break;
+			    }
+			    if (current[0] < 'a' || current[0] > 'z') {
+				cfs_debug("property does not start with [a-z]");
+				result = -EINVAL;
+				break;
+			    }
+			    properties[i] = current;
+			    current[proplen] = '\0'; // ensure property is 0 terminated
+			    remaining -= (proplen + 1);
+			    current += proplen + 1;
+			}
+
+			if (remaining != 0) {
+			    cfs_debug("leftover data after parsing %u properties", rh->num_props);
+			    result = -EINVAL;
+			}
+
+			if (result == 0) {
+			    cfs_debug("cfs_get_guest_config_properties: basic valid checked, do request");
+			    result = cfs_create_guest_conf_properties_msg(outbuf, memdb, properties, rh->num_props, rh->vmid);
+			}
+
+			free(properties);
+		}
 	} else if (request_id == CFS_IPC_VERIFY_TOKEN) {
 
 		cfs_verify_token_request_header_t *rh = (cfs_verify_token_request_header_t *) data;
diff --git a/data/src/status.c b/data/src/status.c
index 9bceaeb..e2bedaa 100644
--- a/data/src/status.c
+++ b/data/src/status.c
@@ -804,25 +804,52 @@ cfs_create_vmlist_msg(GString *str)
 	return 0;
 }
 
-// checks the conf for a line starting with '$prop:' and returns the value
-// afterwards, whitout initial whitespace(s), we only deal with the format
-// restricion imposed by our perl VM config parser, main reference is
+// checks if a config line starts with the given prop. if yes, writes a '\0'
+// at the end of the value, and returns the pointer where the value starts
+char *
+_get_property_value_from_line(char *line, int line_len, const char *prop, int prop_len)
+{
+	if (line_len <= prop_len + 1) return NULL;
+
+	if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
+		char *v_start = &line[prop_len + 1];
+
+		// drop initial value whitespaces here already
+		while (*v_start && isspace(*v_start)) v_start++;
+
+		if (!*v_start) return NULL;
+
+		char *v_end = &line[line_len - 1];
+		while (v_end > v_start && isspace(*v_end)) v_end--;
+		v_end[1] = '\0';
+
+		return v_start;
+	}
+
+	return NULL;
+}
+
+// checks the conf for lines starting with the given props and
+// writes the pointers into the correct positions into the 'found' array
+// afterwards, without initial whitespace(s), we only deal with the format
+// restriction imposed by our perl VM config parser, main reference is
 // PVE::QemuServer::parse_vm_config this allows to be very fast and still
 // relatively simple
-// main restrictions used for our advantage is the properties match reges:
+// main restrictions used for our advantage is the properties match regex:
 // ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) from parse_vm_config
 // currently we only look at the current configuration in place, i.e., *no*
-// snapshort and *no* pending changes
-static char *
-_get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
+// snapshot and *no* pending changes
+void
+_get_property_values(char **found, char *conf, int conf_size, const char **props, uint8_t num_props, char min, char max)
 {
 	const char *const conf_end = conf + conf_size;
 	char *line = conf;
-	size_t remaining_size;
+	size_t remaining_size = conf_size;
+	int count = 0;
 
 	char *next_newline = memchr(conf, '\n', conf_size);
 	if (next_newline == NULL) {
-		return NULL; // valid property lines end with \n, but none in the config
+		return; // valid property lines end with \n, but none in the config
 	}
 	*next_newline = '\0';
 
@@ -830,41 +857,32 @@ _get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
 		if (!line[0]) goto next;
 
 		// snapshot or pending section start, but nothing found yet -> not found
-		if (line[0] == '[') return NULL;
-		// properties start with /^[a-z]/, so continue early if not
-		if (line[0] < 'a' || line[0] > 'z') goto next;
+		if (line[0] == '[') return;
+		// continue early if line does not begin with the min/max char of the properties
+		if (line[0] < min || line[0] > max) goto next;
 
 		int line_len = strlen(line);
-		if (line_len <= prop_len + 1) goto next;
-
-		if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
-			char *v_start = &line[prop_len + 1];
-
-			// drop initial value whitespaces here already
-			while (*v_start && isspace(*v_start)) v_start++;
-
-			if (!*v_start) return NULL;
-
-			char *v_end = &line[line_len - 1];
-			while (v_end > v_start && isspace(*v_end)) v_end--;
-			v_end[1] = '\0';
-
-			return v_start;
+		for (uint8_t i = 0; i < num_props; i++) {
+			char * value = _get_property_value_from_line(line, line_len, props[i], strlen(props[i]));
+			if (value != NULL) {
+				count += (found[i] != NULL) & 0x1; // count newly found lines
+				found[i] = value;
+			}
+		}
+		if (count == num_props) {
+			return; // found all
 		}
 next:
 		line = next_newline + 1;
 		remaining_size = conf_end - line;
-		if (remaining_size <= prop_len) {
-			return NULL;
-		}
 		next_newline = memchr(line, '\n', remaining_size);
 		if (next_newline == NULL) {
-			return NULL; // valid property lines end with \n, but none in the config
+			return; // valid property lines end with \n, but none in the config
 		}
 		*next_newline = '\0';
 	}
 
-	return NULL; // not found
+	return;
 }
 
 static void
@@ -883,24 +901,77 @@ _g_str_append_kv_jsonescaped(GString *str, const char *k, const char *v)
 }
 
 int
-cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
+_print_found_properties(
+	GString *str,
+	gpointer conf,
+	int size,
+	const char **props,
+	uint8_t num_props,
+	uint32_t vmid,
+	char **values,
+	char min,
+	char max,
+	int first)
+{
+	_get_property_values(values, conf, size, props, num_props, min, max);
+
+	uint8_t found = 0;
+	for (uint8_t i = 0; i < num_props; i++) {
+		if (values[i] == NULL) {
+			continue;
+		}
+		if (found) {
+			g_string_append_c(str, ',');
+		} else {
+			if (!first) {
+				g_string_append_printf(str, ",\n");
+			} else {
+				first = 0;
+			}
+			g_string_append_printf(str, "\"%u\":{", vmid);
+			found = 1;
+		}
+		_g_str_append_kv_jsonescaped(str, props[i], values[i]);
+	}
+
+	if (found) {
+		g_string_append_c(str, '}');
+	}
+
+	return first;
+}
+
+int
+cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid)
 {
 	g_return_val_if_fail(cfs_status.vmlist != NULL, -EINVAL);
 	g_return_val_if_fail(str != NULL, -EINVAL);
 
-	int prop_len = strlen(prop);
-	int res = 0;
-	GString *path = NULL;
-
 	// Prelock &memdb->mutex in order to enable the usage of memdb_read_nolock
 	// to prevent Deadlocks as in #2553
 	g_mutex_lock (&memdb->mutex);
 	g_mutex_lock (&mutex);
 
-	g_string_printf(str,"{\n");
+	g_string_printf(str, "{\n");
 
 	GHashTable *ht = cfs_status.vmlist;
+
+	int res = 0;
+	GString *path = NULL;
 	gpointer tmp = NULL;
+	char **values = calloc(num_props, sizeof(char*));
+	char min = 'z', max = 'a';
+
+	for (uint8_t i = 0; i < num_props; i++) {
+		if (props[i][0] > max) {
+			max = props[i][0];
+		}
+
+		if (props[i][0] < min) {
+			min = props[i][0];
+		}
+	}
+
 	if (!g_hash_table_size(ht)) {
 		goto ret;
 	}
@@ -919,15 +990,8 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
 		// use memdb_read_nolock because lock is handled here
 		int size = memdb_read_nolock(memdb, path->str, &tmp);
 		if (tmp == NULL) goto err;
-		if (size <= prop_len) goto ret;
-
-		char *val = _get_property_value(tmp, size, prop, prop_len);
-		if (val == NULL) goto ret;
-
-		g_string_append_printf(str, "\"%u\":{", vmid);
-		_g_str_append_kv_jsonescaped(str, prop, val);
-		g_string_append_c(str, '}');
 
+		_print_found_properties(str, tmp, size, props, num_props, vmid, values, min, max, 1);
 	} else {
 		GHashTableIter iter;
 		g_hash_table_iter_init (&iter, ht);
@@ -943,21 +1007,16 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
 			tmp = NULL;
 			// use memdb_read_nolock because lock is handled here
 			int size = memdb_read_nolock(memdb, path->str, &tmp);
-			if (tmp == NULL || size <= prop_len) continue;
+			if (tmp == NULL) continue;
 
-			char *val = _get_property_value(tmp, size, prop, prop_len);
-			if (val == NULL) continue;
-
-			if (!first) g_string_append_printf(str, ",\n");
-			else first = 0;
-
-			g_string_append_printf(str, "\"%u\":{", vminfo->vmid);
-			_g_str_append_kv_jsonescaped(str, prop, val);
-			g_string_append_c(str, '}');
+			memset(values, 0, sizeof(char*)*num_props); // reset array
+			first = _print_found_properties(str, tmp, size, props, num_props,
+					vminfo->vmid, values, min, max, first);
 		}
 	}
 ret:
 	g_free(tmp);
+	free(values);
 	if (path != NULL) {
 		g_string_free(path, TRUE);
 	}
@@ -973,6 +1032,12 @@ enoent:
 	goto ret;
 }
 
+int
+cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
+{
+	return cfs_create_guest_conf_properties_msg(str, memdb, &prop, 1, vmid);
+}
+
 void
 record_memdb_change(const char *path)
 {
diff --git a/data/src/status.h b/data/src/status.h
index bbf0948..041cb34 100644
--- a/data/src/status.h
+++ b/data/src/status.h
@@ -163,4 +163,7 @@ cfs_create_memberlist_msg(
 int
 cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid);
 
+int
+cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid);
+
 #endif /* _PVE_STATUS_H_ */
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH cluster v9 2/4] Cluster: add get_guest_config_properties
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
                   ` (18 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

akin to get_guest_config_property, but with a list of properties.
uses the new CFS_IPC_GET_GUEST_CONFIG_PROPERTIES

also adds the same NOTEs regarding parsing/permissions to the comment
of get_guest_config_property

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 data/PVE/Cluster.pm | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/data/PVE/Cluster.pm b/data/PVE/Cluster.pm
index abcc46d..99e7975 100644
--- a/data/PVE/Cluster.pm
+++ b/data/PVE/Cluster.pm
@@ -339,10 +339,37 @@ sub get_node_kv {
     return $res;
 }
 
+# properties: an array-ref of config properties you want to get, e.g., this
+# is perfect to get multiple properties of a guest _fast_
+# (>100 faster than manual parsing here)
+# vmid: optional, if a valid is passed we only check that one, else return all
+# NOTE: does *not* searches snapshot and PENDING entries sections!
+# NOTE: returns the guest config lines (excluding trailing whitespace) as is,
+#       so for non-trivial properties, checking the validity must be done
+# NOTE: no permission check is done, that is the responsibilty of the caller
+sub get_guest_config_properties {
+    my ($properties, $vmid) = @_;
+
+    die "properties required" if !defined($properties);
+
+    my $num_props = scalar(@$properties);
+    die "only up to 255 properties supported" if $num_props > 255;
+    my $bindata = pack "VC", $vmid // 0, $num_props;
+    for my $property (@$properties) {
+	$bindata .= pack "Z*", $property;
+    }
+    my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTIES, $bindata);
+
+    return $res;
+}
+
 # property: a config property you want to get, e.g., this is perfect to get
 # the 'lock' entry of a guest _fast_ (>100 faster than manual parsing here)
 # vmid: optional, if a valid is passed we only check that one, else return all
 # NOTE: does *not* searches snapshot and PENDING entries sections!
+# NOTE: returns the guest config lines (excluding trailing whitespace) as is,
+#       so for non-trivial properties, checking the validity must be done
+# NOTE: no permission check is done, that is the responsibilty of the caller
 sub get_guest_config_property {
     my ($property, $vmid) = @_;
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH cluster v9 3/4] datacenter.cfg: add option for tag-style
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 2/4] Cluster: add get_guest_config_properties Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 4/4] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
                   ` (17 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

its a property string containing 'tree-shape' and 'colors'
the colors are formatted like this:
<tag>:<background-color>[:<text-color>]

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* renamed properties
 data/PVE/DataCenterConfig.pm | 37 ++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 6a2adee..532e5e5 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -131,6 +131,29 @@ sub pve_verify_mac_prefix {
     return $mac_prefix;
 }
 
+my $COLOR_RE = '[0-9a-fA-F]{6}';
+my $TAG_COLOR_OVERRIDE_RE = "(?:${PVE::JSONSchema::PVE_TAG_RE}:${COLOR_RE}(?:\:${COLOR_RE})?)";
+
+my $tag_style_format = {
+    'shape' => {
+	optional => 1,
+	type => 'string',
+	enum => ['full', 'circle', 'dense', 'none'],
+	default => 'circle',
+	description => "Tag shape for the web ui tree. 'full' draws the full tag. "
+	    ."'circle' draws only a circle with the background color. "
+	    ."'dense' only draws a small rectancle (useful when many tags are assigned to each guest)."
+	    ."'none' disables showing the tags.",
+    },
+    'color-map' => {
+	optional => 1,
+	type => 'string',
+	pattern => "${TAG_COLOR_OVERRIDE_RE}(?:\;$TAG_COLOR_OVERRIDE_RE)*",
+	typetext => '<tag>:<hex-color>[:<hex-color-for-text>][;<tag>=...]',
+	description => "Manual color mapping for tags (semicolon separated).",
+    },
+};
+
 my $datacenter_schema = {
     type => "object",
     additionalProperties => 0,
@@ -256,6 +279,12 @@ my $datacenter_schema = {
 	    maxLength => 64 * 1024,
 	    optional => 1,
 	},
+	'tag-style' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "Tag style options.",
+	    format => $tag_style_format,
+	},
     },
 };
 
@@ -300,6 +329,10 @@ sub parse_datacenter_config {
 	$res->{webauthn} = parse_property_string($webauthn_format, $webauthn);
     }
 
+    if (my $tag_style = $res->{'tag-style'}) {
+	$res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
+    }
+
     # for backwards compatibility only, new migration property has precedence
     if (defined($res->{migration_unsecure})) {
 	if (defined($res->{migration}->{type})) {
@@ -359,6 +392,10 @@ sub write_datacenter_config {
 	$cfg->{webauthn} = PVE::JSONSchema::print_property_string($webauthn, $webauthn_format);
     }
 
+    if (ref(my $tag_style = $cfg->{'tag-style'})) {
+	$cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
+    }
+
     my $comment = '';
     # add description as comment to top of file
     my $description = $cfg->{description} || '';
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH cluster v9 4/4] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (2 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14 13:32   ` Wolfgang Bumiller
  2022-11-14  9:43 ` [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check Dominik Csapak
                   ` (16 subsequent siblings)
  20 siblings, 1 reply; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

by adding a 'user-tag-privileges' and 'admin-tags' option.
The first sets the policy by which "normal" users (with
'VM.Config.Options' on the respective guest) can create/delete tags
and the second is a list of tags only settable by 'admins'
('Sys.Modify' on '/')

also add a helper 'get_allowed_tags' that returns the allowed (existing)
tags, the privileged tags, and if a user can enter 'freeform' tags.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* renamed properties
* improved descriptions
* renamed get_user_admin_tag to get_allowed_tags and consider the user
  priviliges, also return if we allow 'freeform' tags
* smaller bug fixes (e.g. list reference handling in join)

 data/PVE/DataCenterConfig.pm | 107 +++++++++++++++++++++++++++++++++++
 1 file changed, 107 insertions(+)

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 532e5e5..b689942 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -154,6 +154,32 @@ my $tag_style_format = {
     },
 };
 
+my $user_tag_privs_format = {
+    'user-allow' => {
+	optional => 1,
+	type => 'string',
+	enum => ['none', 'list', 'existing', 'free'],
+	default => 'free',
+	description => "Controls tag usage for users without `Sys.Modify` on `/` bey either "
+	    ."allowing `none`, a `list`, already `existing` or anything (`free`).",
+	verbose_description => "Controls which tags can be set or deleted on resources an user "
+	    ."controls (such as guests). Users iwth the `Sys.Modify` privilege on `/` are always "
+	    ." unrestricted. "
+	    ."'none' means no tags are modifiable. "
+	    ."'list' allows tags from the given list. "
+	    ."'existing' means only already existing tags of resources able to access or from the"
+	    ."given list. "
+	    ."'free' means users can assign any tags."
+    },
+    'user-allow-list' => {
+	optional => 1,
+	type => 'string',
+	pattern => "${PVE::JSONSchema::PVE_TAG_RE}(?:\;${PVE::JSONSchema::PVE_TAG_RE})*",
+	typetext => "<tag>[;<tag>...]",
+	description => "List of tags users are allowed to set and delete (semicolon separated).",
+    },
+};
+
 my $datacenter_schema = {
     type => "object",
     additionalProperties => 0,
@@ -285,12 +311,68 @@ my $datacenter_schema = {
 	    description => "Tag style options.",
 	    format => $tag_style_format,
 	},
+	'user-tag-access' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "Privilege options for user settable tags",
+	    format => $user_tag_privs_format,
+	},
+	'privileged-tags' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "A list of tags that require a `Sys.Modify` on '/') to set and delete. "
+		."Tags set here that are also in 'user-tag-access' also require `Sys.Modify`.",
+	    pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
+	    typetext => "<tag>[;<tag>...]",
+	},
     },
 };
 
 # make schema accessible from outside (for documentation)
 sub get_datacenter_schema { return $datacenter_schema };
 
+# in scalar context, returns the list of allowed tags that exist
+# in list context, returns a tuple of allowed tags, privileged tags, and if freeform is enabled
+#
+# requires an instance of rpcenv and a user to check the correct privileges for the allowed tags
+sub get_allowed_tags {
+    my ($rpcenv, $user) = @_;
+
+    my $privileged_user = $rpcenv->check($user, '/', ['Sys.Modify'], 1);
+
+    my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
+
+    my $freeform = 1;
+    my $allowed_tags = {};
+    my $privileged_tags = {};
+    if (my $tags = $dc->{'privileged-tags'}) {
+	$privileged_tags->{$_} = 1 for $tags->@*;
+    }
+    my $user_tag_privs = $dc->{'user-tag-access'} // {};
+    my $user_allow = $user_tag_privs->{'user-allow'} // 'free';
+    $freeform = $user_allow eq 'free';
+
+    if ($user_allow ne 'none' || $privileged_user) {
+	map { $allowed_tags->{$_} = 1 } ($user_tag_privs->{'user-allow-list'} // [])->@*;
+    }
+
+    if ($user_allow eq 'free' || $user_allow eq 'existing' || $privileged_user) {
+	my $props = PVE::Cluster::get_guest_config_properties(['tags']);
+	for my $vmid (keys $props->%*) {
+	    next if !$privileged_user && !$rpcenv->check_vm_perm($user, $vmid, undef, ['VM.Audit'], 1, 1);
+	    map { $allowed_tags->{$_} = 1 } PVE::Tools::split_list($props->{$vmid}->{tags});
+	}
+    }
+
+    if ($privileged_user) {
+	$allowed_tags->{$_} = 1 for keys $privileged_tags->%*;
+    } else {
+	delete $allowed_tags->{$_} for keys $privileged_tags->%*;
+    }
+
+    return wantarray ? ($allowed_tags, $privileged_tags, $freeform) : $allowed_tags;
+}
+
 sub parse_datacenter_config {
     my ($filename, $raw) = @_;
 
@@ -333,6 +415,19 @@ sub parse_datacenter_config {
 	$res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
     }
 
+    if (my $user_tag_privs = $res->{'user-tag-access'}) {
+	$res->{'user-tag-access'} =
+	    parse_property_string($user_tag_privs_format, $user_tag_privs);
+
+	    if (my $user_tags = $res->{'user-tag-access'}->{'user-allow-list'}) {
+	        $res->{'user-tag-access'}->{'user-allow-list'} = [split(';', $user_tags)];
+	    }
+    }
+
+    if (my $admin_tags = $res->{'privileged-tags'}) {
+	$res->{'privileged-tags'} = [split(';', $admin_tags)];
+    }
+
     # for backwards compatibility only, new migration property has precedence
     if (defined($res->{migration_unsecure})) {
 	if (defined($res->{migration}->{type})) {
@@ -396,6 +491,18 @@ sub write_datacenter_config {
 	$cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
     }
 
+    if (ref(my $user_tag_privs = $cfg->{'user-tag-access'})) {
+	if (my $user_tags = $user_tag_privs->{'user-allow-list'}) {
+	    $user_tag_privs->{'user-allow-list'} = join(';', sort $user_tags->@*);
+	}
+	$cfg->{'user-tag-access'} =
+	    PVE::JSONSchema::print_property_string($user_tag_privs, $user_tag_privs_format);
+    }
+
+    if (ref(my $admin_tags = $cfg->{'privileged-tags'})) {
+	$cfg->{'privileged-tags'} = join(';', sort $admin_tags->@*);
+    }
+
     my $comment = '';
     # add description as comment to top of file
     my $description = $cfg->{description} || '';
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (3 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 4/4] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14 13:37   ` Wolfgang Bumiller
  2022-11-15  8:34   ` Aaron Lauterer
  2022-11-14  9:43 ` [pve-devel] [PATCH container v9 1/1] check_ct_modify_config_perm: " Dominik Csapak
                   ` (15 subsequent siblings)
  20 siblings, 2 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

'normal' tags require 'VM.Config.Options' on '/vm/<vmid>', but not
allowed tags (either limited with 'user-tag-access' or 'privileged-tags'
in the datacenter config) require 'Sys.Modify' on '/'

this patch implements the proper checks on adding/editing/deleting
these permissions

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* adapt to 'get_allowed_tags'
* cache privilege checks (so we don't have to do it when many tags change)
 PVE/API2/Qemu.pm | 53 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 52 insertions(+), 1 deletion(-)

diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index 30348e6..269451c 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -539,7 +539,6 @@ my $generaloptions = {
     'startup' => 1,
     'tdf' => 1,
     'template' => 1,
-    'tags' => 1,
 };
 
 my $vmpoweroptions = {
@@ -609,6 +608,7 @@ my $check_vm_modify_config_perm = sub {
 	next if PVE::QemuServer::is_valid_drivename($opt);
 	next if $opt eq 'cdrom';
 	next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
+	next if $opt eq 'tags';
 
 
 	if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
@@ -1631,6 +1631,31 @@ my $update_vm_api  = sub {
 		}
 	    };
 
+	    my $allowed_tags;
+	    my $privileged_tags;
+	    my $freeform;
+	    my $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1);
+	    my $has_config_options =
+		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'], 1);
+
+	    my $check_tag_perms = sub {
+		my ($tag) = @_;
+
+		return if $privileged_user;
+
+		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'])
+		    if !$has_config_options;
+
+		if (!defined($allowed_tags) && !defined($privileged_tags) && !defined($freeform)) {
+		    ($allowed_tags, $privileged_tags, $freeform)
+			= PVE::DataCenterConfig::get_allowed_tags($rpcenv, $authuser);
+		}
+
+		if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
+		    die "'Sys.Modify' required on '/' for modifying tag $tag\n" if !$privileged_user;
+		}
+	    };
+
 	    foreach my $opt (@delete) {
 		$modified->{$opt} = 1;
 		$conf = PVE::QemuConfig->load_config($vmid); # update/reload
@@ -1689,6 +1714,13 @@ my $update_vm_api  = sub {
 		    }
 		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
 		    PVE::QemuConfig->write_config($vmid, $conf);
+		} elsif ($opt eq 'tags') {
+		    foreach my $tag (PVE::Tools::split_list($val)) {
+			check_tag_perms->($tag);
+		    }
+
+		    delete $conf->{$opt};
+		    PVE::QemuConfig->write_config($vmid, $conf);
 		} else {
 		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
 		    PVE::QemuConfig->write_config($vmid, $conf);
@@ -1748,6 +1780,25 @@ my $update_vm_api  = sub {
 		    } elsif ($authuser ne 'root@pam') {
 			die "only root can modify '$opt' config for real devices\n";
 		    }
+		    $conf->{pending}->{$opt} = $param->{$opt};
+		} elsif ($opt eq 'tags') {
+		    my $old_tags = {};
+		    my $new_tags = {};
+
+		    map { $old_tags->{$_} += 1 } PVE::Tools::split_list($conf->{$opt} // '');
+		    map { $new_tags->{$_} += 1 } PVE::Tools::split_list($param->{$opt});
+
+		    my $check_tags = sub {
+			my ($a, $b) = @_;
+			foreach my $tag (keys %$a) {
+			    next if ($b->{$tag} // 0) == ($a->{$tag} // 0);
+			    $check_tag_perms->($tag);
+			}
+		    };
+
+		    $check_tags->($old_tags, $new_tags);
+		    $check_tags->($new_tags, $old_tags);
+
 		    $conf->{pending}->{$opt} = $param->{$opt};
 		} else {
 		    $conf->{pending}->{$opt} = $param->{$opt};
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH container v9 1/1] check_ct_modify_config_perm: improve tag privilege check
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (4 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14 13:37   ` Wolfgang Bumiller
  2022-11-14  9:43 ` [pve-devel] [PATCH widget-toolkit v9 1/2] add tag related helpers Dominik Csapak
                   ` (14 subsequent siblings)
  20 siblings, 1 reply; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

'normal' tags require 'VM.Config.Options' on '/vm/<vmid>', but not
allowed tags (either limited with 'user-tag-access' or 'privileged-tags'
in the datacenter config) require 'Sys.Modify' on '/'

this patch implements the proper checks on adding/editing/deleting
these permissions

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* adapt to 'get_allowed_tags'
* cache privilege checks (so we don't have to do it when many tags change)
 src/PVE/LXC.pm | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 4bbd739..44dee7e 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -1336,6 +1336,48 @@ sub check_ct_modify_config_perm {
 	} elsif ($opt eq 'hookscript') {
 	    # For now this is restricted to root@pam
 	    raise_perm_exc("changing the hookscript is only allowed for root\@pam");
+	} elsif ($opt eq 'tags') {
+	    my $allowed_tags;
+	    my $privileged_tags;
+	    my $freeform;
+	    my $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1);
+	    my $has_config_options =
+		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'], 1);
+
+	    my $check_tag_perms = sub {
+		my ($tag) = @_;
+
+		return if $privileged_user;
+
+		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'])
+		    if !$has_config_options;
+
+		if (!defined($allowed_tags) && !defined($privileged_tags) && !defined($freeform)) {
+		    ($allowed_tags, $privileged_tags, $freeform)
+			= PVE::DataCenterConfig::get_allowed_tags($rpcenv, $authuser);
+		}
+
+		if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
+		    die "'Sys.Modify' required on '/' for modifying tag $tag\n" if !$privileged_user;
+		}
+	    };
+
+	    my $old_tags = {};
+	    my $new_tags = {};
+
+	    map { $old_tags->{$_} += 1 } PVE::Tools::split_list($oldconf->{$opt} // '');
+	    map { $new_tags->{$_} += 1 } PVE::Tools::split_list($newconf->{$opt});
+
+	    my $check_tags = sub {
+		my ($a, $b) = @_;
+		foreach my $tag (keys %$a) {
+		    next if ($b->{$tag} // 0) == ($a->{$tag} // 0);
+		    $check_tag_perms->($tag);
+		}
+	    };
+
+	    $check_tags->($old_tags, $new_tags);
+	    $check_tags->($new_tags, $old_tags);
 	} else {
 	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
 	}
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH widget-toolkit v9 1/2] add tag related helpers
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (5 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH container v9 1/1] check_ct_modify_config_perm: " Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH widget-toolkit v9 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
                   ` (13 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

helpers to
* generate a color from a string consistently
* generate a html tag for a tag
* related css classes

contrast is calculated according to SAPC draft:
https://github.com/Myndex/SAPC-APCA

which is likely to become a w3c guideline in the future and seems
to be a better algorithm for this

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/Utils.js         | 88 ++++++++++++++++++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 45 ++++++++++++++++++++++
 2 files changed, 133 insertions(+)

diff --git a/src/Utils.js b/src/Utils.js
index e9b84f8..7255e3f 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1272,6 +1272,94 @@ utilities: {
 	    .map(val => val.charCodeAt(0)),
 	);
     },
+
+    stringToRGB: function(string) {
+	let hash = 0;
+	if (!string) {
+	    return hash;
+	}
+	string += 'prox'; // give short strings more variance
+	for (let i = 0; i < string.length; i++) {
+	    hash = string.charCodeAt(i) + ((hash << 5) - hash);
+	    hash = hash & hash; // to int
+	}
+
+	let alpha = 0.7; // make the color a bit brighter
+	let bg = 255; // assume white background
+
+	return [
+	    (hash & 255) * alpha + bg * (1 - alpha),
+	    ((hash >> 8) & 255) * alpha + bg * (1 - alpha),
+	    ((hash >> 16) & 255) * alpha + bg * (1 - alpha),
+	];
+    },
+
+    rgbToCss: function(rgb) {
+	return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
+    },
+
+    rgbToHex: function(rgb) {
+	let r = Math.round(rgb[0]).toString(16);
+	let g = Math.round(rgb[1]).toString(16);
+	let b = Math.round(rgb[2]).toString(16);
+	return `${r}${g}${b}`;
+    },
+
+    hexToRGB: function(hex) {
+	if (!hex) {
+	    return undefined;
+	}
+	if (hex.length === 7) {
+	    hex = hex.slice(1);
+	}
+	let r = parseInt(hex.slice(0, 2), 16);
+	let g = parseInt(hex.slice(2, 4), 16);
+	let b = parseInt(hex.slice(4, 6), 16);
+	return [r, g, b];
+    },
+
+    // optimized & simplified SAPC function
+    // https://github.com/Myndex/SAPC-APCA
+    getTextContrastClass: function(rgb) {
+	    const blkThrs = 0.022;
+	    const blkClmp = 1.414;
+
+	    // linearize & gamma correction
+	    let r = (rgb[0] / 255) ** 2.4;
+	    let g = (rgb[1] / 255) ** 2.4;
+	    let b = (rgb[2] / 255) ** 2.4;
+
+	    // relative luminance sRGB
+	    let bg = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
+
+	    // black clamp
+	    bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp;
+
+	    // SAPC with white text
+	    let contrastLight = bg ** 0.65 - 1;
+	    // SAPC with black text
+	    let contrastDark = bg ** 0.56 - 0.046134502;
+
+	    if (Math.abs(contrastLight) >= Math.abs(contrastDark)) {
+		return 'light';
+	    } else {
+		return 'dark';
+	    }
+    },
+
+    getTagElement: function(string, color_overrides) {
+	let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
+	let style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`;
+	let cls;
+	if (rgb.length > 3) {
+	    style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`;
+	    cls = "proxmox-tag-dark";
+	} else {
+	    let txtCls = Proxmox.Utils.getTextContrastClass(rgb);
+	    cls = `proxmox-tag-${txtCls}`;
+	}
+	return `<span class="${cls}" style="${style}">${string}</span>`;
+    },
 },
 
     singleton: true,
diff --git a/src/css/ext6-pmx.css b/src/css/ext6-pmx.css
index 231b4ce..bb85ffb 100644
--- a/src/css/ext6-pmx.css
+++ b/src/css/ext6-pmx.css
@@ -6,6 +6,51 @@
     background-color: LightYellow;
 }
 
+.proxmox-tags-full .proxmox-tag-light,
+.proxmox-tags-full .proxmox-tag-dark {
+    border-radius: 3px;
+    padding: 1px 6px;
+    margin: 0px 1px;
+}
+
+.proxmox-tags-circle .proxmox-tag-light,
+.proxmox-tags-circle .proxmox-tag-dark {
+    margin: 0px 1px;
+    position: relative;
+    top: 2px;
+    border-radius: 6px;
+    height: 12px;
+    width: 12px;
+    display: inline-block;
+    color: transparent !important;
+    overflow: hidden;
+}
+
+.proxmox-tags-none .proxmox-tag-light,
+.proxmox-tags-none .proxmox-tag-dark {
+    display: none;
+}
+
+.proxmox-tags-dense .proxmox-tag-light,
+.proxmox-tags-dense .proxmox-tag-dark {
+    width: 6px;
+    margin-right: 1px;
+    display: inline-block;
+    color: transparent !important;
+    overflow: hidden;
+    vertical-align: bottom;
+}
+
+.proxmox-tags-full .proxmox-tag-light {
+    color: #fff;
+    background-color: #383838;
+}
+
+.proxmox-tags-full .proxmox-tag-dark {
+    color: #000;
+    background-color: #f0f0f0;
+}
+
 .x-mask-msg-text {
     text-align: center;
 }
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH widget-toolkit v9 2/2] Toolkit: add override for Ext.dd.DragDropManager
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (6 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH widget-toolkit v9 1/2] add tag related helpers Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 01/12] api: /cluster/resources: add tags to returned properties Dominik Csapak
                   ` (12 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

to fix selection behavior for Ext.dd.DragZone.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/Toolkit.js | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/src/Toolkit.js b/src/Toolkit.js
index 4381f46..2f4a01a 100644
--- a/src/Toolkit.js
+++ b/src/Toolkit.js
@@ -686,6 +686,22 @@ Ext.define('Proxmox.view.DragZone', {
     },
 });
 
+// Fix text selection on drag when using DragZone,
+// see https://forum.sencha.com/forum/showthread.php?335100
+Ext.define('Proxmox.dd.DragDropManager', {
+    override: 'Ext.dd.DragDropManager',
+
+    stopEvent: function(e) {
+	if (this.stopPropagation) {
+	    e.stopPropagation();
+	}
+
+	if (this.preventDefault) {
+	    e.preventDefault();
+	}
+    },
+});
+
 // force alert boxes to be rendered with an Error Icon
 // since Ext.Msg is an object and not a prototype, we need to override it
 // after the framework has been initiated
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 01/12] api: /cluster/resources: add tags to returned properties
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (7 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH widget-toolkit v9 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 02/12] api: add /ui-options api call Dominik Csapak
                   ` (11 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

by querying 'lock' and 'tags' with 'get_guest_config_properties'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 PVE/API2/Cluster.pm | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index 49e319a5c..34a6e08cd 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -371,7 +371,8 @@ __PACKAGE__->register_method({
 
 	# we try to generate 'numbers' by using "$X + 0"
 	if (!$param->{type} || $param->{type} eq 'vm') {
-	    my $locked_vms = PVE::Cluster::get_guest_config_property('lock');
+	    my $prop_list = [qw(lock tags)];
+	    my $props = PVE::Cluster::get_guest_config_properties($prop_list);
 
 	    for my $vmid (sort keys %$idlist) {
 
@@ -403,8 +404,10 @@ __PACKAGE__->register_method({
 		# only skip now to next to ensure that the pool stats above are filled, if eligible
 		next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
 
-		if (defined(my $lock = $locked_vms->{$vmid}->{lock})) {
-		    $entry->{lock} = $lock;
+		for my $prop (@$prop_list) {
+		    if (defined(my $value = $props->{$vmid}->{$prop})) {
+			$entry->{$prop} = $value;
+		    }
 		}
 
 		if (defined($entry->{pool}) &&
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 02/12] api: add /ui-options api call
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (8 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 01/12] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
                   ` (10 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

which contains ui relevant options, like the console preference and tag-style
also contains the list of allowed tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* add 'allowed-tags' too

maybe we want the 'allowed-tags' somewhere else? though we only use
it for the ui at the moment and i did not want to introduce another api
call that we have to call after logging in

 PVE/API2.pm | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 56 insertions(+)

diff --git a/PVE/API2.pm b/PVE/API2.pm
index a42561604..03b16aaf4 100644
--- a/PVE/API2.pm
+++ b/PVE/API2.pm
@@ -118,6 +118,7 @@ __PACKAGE__->register_method ({
 
 	my $res = {};
 
+	# TODO remove with next major release
 	my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
 	for my $k (qw(console)) {
 	    $res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
@@ -130,4 +131,59 @@ __PACKAGE__->register_method ({
 	return $res;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'ui-options',
+    path => 'ui-options',
+    method => 'GET',
+    permissions => { user => 'all' },
+    description => "Global options regarding the UI.",
+    parameters => {
+	additionalProperties => 0,
+	properties => {},
+    },
+    returns => {
+	type => "object",
+	properties => {
+	    console => {
+		type => 'string',
+		enum => ['applet', 'vv', 'html5', 'xtermjs'],
+		optional => 1,
+		description => 'The default console viewer to use.',
+	    },
+	    'tag-style' => {
+		type => 'string',
+		optional => 1,
+		description => 'Cluster wide tag style overrides',
+	    },
+	    'allowed-tags' => {
+		type => 'array',
+		optional => 1,
+		description => 'A list of allowed tags that exist.',
+		items => {
+		    type => 'string',
+		    description => 'A tag the user is allowed to set.',
+		},
+	    },
+	},
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $res = {};
+
+	my $rpcenv = PVE::RPCEnvironment::get();
+	my $authuser = $rpcenv->get_user();
+
+	my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
+	for my $k (qw(console tag-style)) {
+	    $res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
+	}
+
+	my $tags = PVE::DataCenterConfig::get_allowed_tags($rpcenv, $authuser);
+
+	$res->{'allowed-tags'} = [sort keys $tags->%*];
+
+	return $res;
+    }});
+
 1;
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (9 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 02/12] api: add /ui-options api call Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 04/12] ui: parse and save tag infos from /ui-options Dominik Csapak
                   ` (9 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

and move the use of the console from VersionInfo to here, since
this will be the future place for ui related backend options.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* moved the logic into Utils, since we'll want to use that more
  regularly
 www/manager6/Utils.js         | 12 +++++++++++-
 www/manager6/Workspace.js     |  2 ++
 www/manager6/dc/OptionView.js |  4 ++--
 3 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index adcf082ff..29bf667b4 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1332,7 +1332,7 @@ Ext.define('PVE.Utils', {
 	    allowSpice = consoles.spice;
 	    allowXtermjs = !!consoles.xtermjs;
 	}
-	let dv = PVE.VersionInfo.console || (type === 'kvm' ? 'vv' : 'xtermjs');
+	let dv = PVE.UIOptions.console || (type === 'kvm' ? 'vv' : 'xtermjs');
 	if (dv === 'vv' && !allowSpice) {
 	    dv = allowXtermjs ? 'xtermjs' : 'html5';
 	} else if (dv === 'xtermjs' && !allowXtermjs) {
@@ -1854,6 +1854,16 @@ Ext.define('PVE.Utils', {
     },
 
     notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
+
+    updateUIOptions: function() {
+	Proxmox.Utils.API2Request({
+	    url: '/ui-options',
+	    method: 'GET',
+	    success: function(response) {
+		PVE.UIOptions = response?.result?.data ?? {};
+	    },
+	});
+    },
 },
 
     singleton: true,
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index 2bb502e0c..a7423508e 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -158,6 +158,8 @@ Ext.define('PVE.StdWorkspace', {
 		},
 	    });
 
+	    PVE.Utils.updateUIOptions();
+
 	    Proxmox.Utils.API2Request({
 		url: '/cluster/sdn',
 		method: 'GET',
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index 5a2be182e..ff96351d5 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -343,9 +343,9 @@ Ext.define('PVE.dc.OptionView', {
 	    }
 
 	    var rec = store.getById('console');
-	    PVE.VersionInfo.console = rec.data.value;
+	    PVE.UIOptions.console = rec.data.value;
 	    if (rec.data.value === '__default__') {
-		delete PVE.VersionInfo.console;
+		delete PVE.UIOptions.console;
 	    }
 	});
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 04/12] ui: parse and save tag infos from /ui-options
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (10 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 05/12] ui: add form/TagColorGrid Dominik Csapak
                   ` (8 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

stores the color-map into a global list of overrides. on update, also parse
the values from the browser localstore. Also emits a GlobalEvent
'loadedUiOptions' so that e.g. the tags can listen to that and refresh their
colors

also saves the list of 'allowed-tags' into PVE.Utils

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* also parses the allowed tag into the taglist now (and don't parse it
  from the tree anymore, since that should be the same)
 www/manager6/Utils.js | 60 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 59 insertions(+), 1 deletion(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 29bf667b4..9cd3bd6c2 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1860,10 +1860,68 @@ Ext.define('PVE.Utils', {
 	    url: '/ui-options',
 	    method: 'GET',
 	    success: function(response) {
-		PVE.UIOptions = response?.result?.data ?? {};
+		PVE.UIOptions = response?.result?.data ?? {
+		    'allowed-tags': [],
+		};
+		let colors = PVE.UIOptions?.['tag-style']?.['color-map'];
+		let shape = PVE.UIOptions?.['tag-style']?.shape;
+
+		PVE.Utils.updateTagSettings(colors, shape);
+		PVE.Utils.updateTagList(PVE.UIOptions['allowed-tags']);
+		if (colors) {
+		    // refresh tree once
+		    PVE.data.ResourceStore.fireEvent('load');
+		    Ext.GlobalEvents.fireEvent('loadedUiOptions');
+		}
 	    },
 	});
     },
+
+    tagList: new Set(),
+
+    updateTagList: function(tags) {
+	PVE.Utils.tagList = [...new Set([...tags])].sort();
+    },
+
+    parseTagOverrides: function(overrides) {
+	let colors = {};
+	(overrides || "").split(';').forEach(color => {
+	    if (!color) {
+		return;
+	    }
+	    let [tag, color_hex, font_hex] = color.split(':');
+	    let r = parseInt(color_hex.slice(0, 2), 16);
+	    let g = parseInt(color_hex.slice(2, 4), 16);
+	    let b = parseInt(color_hex.slice(4, 6), 16);
+	    colors[tag] = [r, g, b];
+	    if (font_hex) {
+		colors[tag].push(parseInt(font_hex.slice(0, 2), 16));
+		colors[tag].push(parseInt(font_hex.slice(2, 4), 16));
+		colors[tag].push(parseInt(font_hex.slice(4, 6), 16));
+	    }
+	});
+	return colors;
+    },
+
+    tagOverrides: {},
+
+    updateTagOverrides: function(colors) {
+	let sp = Ext.state.Manager.getProvider();
+	let color_state = sp.get('colors', '');
+	let browser_colors = PVE.Utils.parseTagOverrides(color_state);
+	PVE.Utils.tagOverrides = Ext.apply({}, browser_colors, colors);
+	Ext.GlobalEvents.fireEvent('tag-color-override');
+    },
+
+    updateTagSettings: function(overrides, style) {
+	PVE.Utils.updateTagOverrides(PVE.Utils.parseTagOverrides(overrides ?? ""));
+
+	if (style === undefined || style === '__default__') {
+	    style = 'circle';
+	}
+
+	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
+    },
 },
 
     singleton: true,
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 05/12] ui: add form/TagColorGrid
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (11 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 04/12] ui: parse and save tag infos from /ui-options Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 06/12] ui: add PVE.form.ListField Dominik Csapak
                   ` (7 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

this provides a basic grid to edit a list of tag color overrides.
We'll use this for editing the datacenter.cfg overrides and the
browser storage overrides.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/css/ext6-pve.css              |   5 +
 www/manager6/Makefile             |   1 +
 www/manager6/Utils.js             |   2 +
 www/manager6/form/TagColorGrid.js | 357 ++++++++++++++++++++++++++++++
 4 files changed, 365 insertions(+)
 create mode 100644 www/manager6/form/TagColorGrid.js

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index dadb84a97..f7d0c4201 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -651,3 +651,8 @@ table.osds td:first-of-type {
     background-color: rgb(245, 245, 245);
     color: #000;
 }
+
+.x-pveColorPicker-default-cell > .x-grid-cell-inner {
+    padding-top: 0px;
+    padding-bottom: 0px;
+}
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 5938c7f5c..942583e61 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -73,6 +73,7 @@ JSSRC= 							\
 	form/VNCKeyboardSelector.js			\
 	form/ViewSelector.js				\
 	form/iScsiProviderSelector.js			\
+	form/TagColorGrid.js				\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 9cd3bd6c2..2f8cdec77 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1922,6 +1922,8 @@ Ext.define('PVE.Utils', {
 
 	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
     },
+
+    tagCharRegex: /^[a-z0-9+_.-]$/i,
 },
 
     singleton: true,
diff --git a/www/manager6/form/TagColorGrid.js b/www/manager6/form/TagColorGrid.js
new file mode 100644
index 000000000..3ad8e07f0
--- /dev/null
+++ b/www/manager6/form/TagColorGrid.js
@@ -0,0 +1,357 @@
+Ext.define('PVE.form.ColorPicker', {
+    extend: 'Ext.form.FieldContainer',
+    alias: 'widget.pveColorPicker',
+
+    defaultBindProperty: 'value',
+
+    config: {
+	value: null,
+    },
+
+    height: 24,
+
+    layout: {
+	type: 'hbox',
+	align: 'stretch',
+    },
+
+    getValue: function() {
+	return this.realvalue.slice(1);
+    },
+
+    setValue: function(value) {
+	let me = this;
+	me.setColor(value);
+	if (value && value.length === 6) {
+	    me.picker.value = value[0] !== '#' ? `#${value}` : value;
+	}
+    },
+
+    setColor: function(value) {
+	let me = this;
+	let oldValue = me.realvalue;
+	me.realvalue = value;
+	let color = value.length === 6 ? `#${value}` : undefined;
+	me.down('#picker').setStyle('background-color', color);
+	me.down('#text').setValue(value ?? "");
+	me.fireEvent('change', me, me.realvalue, oldValue);
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.picker = document.createElement('input');
+	me.picker.type = 'color';
+	me.picker.style = `opacity: 0; border: 0px; width: 100%; height: ${me.height}px`;
+	me.picker.value = `${me.value}`;
+
+	me.items = [
+	    {
+		xtype: 'textfield',
+		itemId: 'text',
+		minLength: !me.allowBlank ? 6 : undefined,
+		maxLength: 6,
+		enforceMaxLength: true,
+		allowBlank: me.allowBlank,
+		emptyText: me.allowBlank ? gettext('Automatic') : undefined,
+		maskRe: /[a-f0-9]/i,
+		regex: /^[a-f0-9]{6}$/i,
+		flex: 1,
+		listeners: {
+		    change: function(field, value) {
+			me.setValue(value);
+		    },
+		},
+	    },
+	    {
+		xtype: 'box',
+		style: {
+		    'margin-left': '1px',
+		    border: '1px solid #cfcfcf',
+		},
+		itemId: 'picker',
+		width: 24,
+		contentEl: me.picker,
+	    },
+	];
+
+	me.callParent();
+	me.picker.oninput = function() {
+	    me.setColor(me.picker.value.slice(1));
+	};
+    },
+});
+
+Ext.define('PVE.form.TagColorGrid', {
+    extend: 'Ext.grid.Panel',
+    alias: 'widget.pveTagColorGrid',
+
+    mixins: [
+	'Ext.form.field.Field',
+    ],
+
+    allowBlank: true,
+    selectAll: false,
+    isFormField: true,
+    deleteEmpty: false,
+    selModel: 'checkboxmodel',
+
+    config: {
+	deleteEmpty: false,
+    },
+
+    emptyText: gettext('No Overrides'),
+    viewConfig: {
+	deferEmptyText: false,
+    },
+
+    setValue: function(value) {
+	let me = this;
+	let colors;
+	if (Ext.isObject(value)) {
+	    colors = value.colors;
+	} else {
+	    colors = value;
+	}
+	if (!colors) {
+	    me.getStore().removeAll();
+	    me.checkChange();
+	    return me;
+	}
+	let entries = (colors.split(';') || []).map((entry) => {
+	    let [tag, bg, fg] = entry.split(':');
+	    fg = fg || "";
+	    return {
+		tag,
+		color: bg,
+		text: fg,
+	    };
+	});
+	me.getStore().setData(entries);
+	me.checkChange();
+	return me;
+    },
+
+    getValue: function() {
+	let me = this;
+	let values = [];
+	me.getStore().each((rec) => {
+	    if (rec.data.tag) {
+		let val = `${rec.data.tag}:${rec.data.color}`;
+		if (rec.data.text) {
+		    val += `:${rec.data.text}`;
+		}
+		values.push(val);
+	    }
+	});
+	return values.join(';');
+    },
+
+    getErrors: function(value) {
+	let me = this;
+	let emptyTag = false;
+	let notValidColor = false;
+	let colorRegex = new RegExp(/^[0-9a-f]{6}$/i);
+	me.getStore().each((rec) => {
+	    if (!rec.data.tag) {
+		emptyTag = true;
+	    }
+	    if (!rec.data.color?.match(colorRegex)) {
+		notValidColor = true;
+	    }
+	    if (rec.data.text && !rec.data.text?.match(colorRegex)) {
+		notValidColor = true;
+	    }
+	});
+	let errors = [];
+	if (emptyTag) {
+	    errors.push(gettext('Tag must not be empty.'));
+	}
+	if (notValidColor) {
+	    errors.push(gettext('Not a valid color.'));
+	}
+	return errors;
+    },
+
+    // override framework function to implement deleteEmpty behaviour
+    getSubmitData: function() {
+	let me = this,
+	    data = null,
+	    val;
+	if (!me.disabled && me.submitValue) {
+	    val = me.getValue();
+	    if (val !== null && val !== '') {
+		data = {};
+		data[me.getName()] = val;
+	    } else if (me.getDeleteEmpty()) {
+		data = {};
+		data.delete = me.getName();
+	    }
+	}
+	return data;
+    },
+
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	addLine: function() {
+	    let me = this;
+	    me.getView().getStore().add({
+		tag: '',
+		color: '',
+		text: '',
+	    });
+	},
+
+	removeSelection: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let selection = view.getSelection();
+	    if (selection === undefined) {
+		return;
+	    }
+
+	    selection.forEach((sel) => {
+		view.getStore().remove(sel);
+	    });
+	    view.checkChange();
+	},
+
+	tagChange: function(field, newValue, oldValue) {
+	    let me = this;
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+	    if (newValue && newValue !== oldValue) {
+		let newrgb = Proxmox.Utils.stringToRGB(newValue);
+		let newvalue = Proxmox.Utils.rgbToHex(newrgb);
+		if (!rec.get('color')) {
+		    rec.set('color', newvalue);
+		} else if (oldValue) {
+		    let oldrgb = Proxmox.Utils.stringToRGB(oldValue);
+		    let oldvalue = Proxmox.Utils.rgbToHex(oldrgb);
+		    if (rec.get('color') === oldvalue) {
+			rec.set('color', newvalue);
+		    }
+		}
+	    }
+	    me.fieldChange(field, newValue, oldValue);
+	},
+
+	backgroundChange: function(field, newValue, oldValue) {
+	    let me = this;
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+	    if (newValue && newValue !== oldValue) {
+		let newrgb = Proxmox.Utils.hexToRGB(newValue);
+		let newcls = Proxmox.Utils.getTextContrastClass(newrgb);
+		let hexvalue = newcls === 'dark' ? '000000' : 'FFFFFF';
+		if (!rec.get('text')) {
+		    rec.set('text', hexvalue);
+		} else if (oldValue) {
+		    let oldrgb = Proxmox.Utils.hexToRGB(oldValue);
+		    let oldcls = Proxmox.Utils.getTextContrastClass(oldrgb);
+		    let oldvalue = oldcls === 'dark' ? '000000' : 'FFFFFF';
+		    if (rec.get('text') === oldvalue) {
+			rec.set('text', hexvalue);
+		    }
+		}
+	    }
+	    me.fieldChange(field, newValue, oldValue);
+	},
+
+	fieldChange: function(field, newValue, oldValue) {
+	    let me = this;
+	    let view = me.getView();
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+	    let column = field.getWidgetColumn();
+	    rec.set(column.dataIndex, newValue);
+	    view.checkChange();
+	},
+    },
+
+    tbar: [
+	{
+	    text: gettext('Add'),
+	    handler: 'addLine',
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Remove'),
+	    handler: 'removeSelection',
+	    disabled: true,
+	},
+    ],
+
+    columns: [
+	{
+	    header: 'Tag',
+	    dataIndex: 'tag',
+	    xtype: 'widgetcolumn',
+	    onWidgetAttach: function(col, widget, rec) {
+		widget.getStore().setData(PVE.Utils.tagList.map(v => ({ tag: v })));
+	    },
+	    widget: {
+		xtype: 'combobox',
+		isFormField: false,
+		maskRe: PVE.Utils.tagCharRegex,
+		allowBlank: false,
+		queryMode: 'local',
+		displayField: 'tag',
+		valueField: 'tag',
+		store: {},
+		listeners: {
+		    change: 'tagChange',
+		},
+	    },
+	    flex: 1,
+	},
+	{
+	    header: gettext('Background'),
+	    xtype: 'widgetcolumn',
+	    flex: 1,
+	    dataIndex: 'color',
+	    widget: {
+		xtype: 'pveColorPicker',
+		isFormField: false,
+		listeners: {
+		    change: 'backgroundChange',
+		},
+	    },
+	},
+	{
+	    header: gettext('Text'),
+	    xtype: 'widgetcolumn',
+	    flex: 1,
+	    dataIndex: 'text',
+	    widget: {
+		xtype: 'pveColorPicker',
+		allowBlank: true,
+		isFormField: false,
+		listeners: {
+		    change: 'fieldChange',
+		},
+	    },
+	},
+    ],
+
+    store: {
+	listeners: {
+	    update: function() {
+		this.commitChanges();
+	    },
+	},
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.callParent();
+	me.initField();
+    },
+});
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 06/12] ui: add PVE.form.ListField
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (12 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 05/12] ui: add form/TagColorGrid Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 07/12] ui: dc/OptionView: add editors for tag settings Dominik Csapak
                   ` (6 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

a field which contains just a list of textfields, e.g. useful for a list
of simple tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
new in v9
 www/manager6/Makefile          |   1 +
 www/manager6/form/ListField.js | 165 +++++++++++++++++++++++++++++++++
 2 files changed, 166 insertions(+)
 create mode 100644 www/manager6/form/ListField.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 942583e61..9c7ced918 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -74,6 +74,7 @@ JSSRC= 							\
 	form/ViewSelector.js				\
 	form/iScsiProviderSelector.js			\
 	form/TagColorGrid.js				\
+	form/ListField.js				\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/form/ListField.js b/www/manager6/form/ListField.js
new file mode 100644
index 000000000..faa8e168b
--- /dev/null
+++ b/www/manager6/form/ListField.js
@@ -0,0 +1,165 @@
+Ext.define('PVE.form.ListField', {
+    extend: 'Ext.grid.Panel',
+    alias: 'widget.pveListField',
+
+    mixins: [
+	'Ext.form.field.Field',
+    ],
+
+    // override for column header
+    fieldTitle: gettext('Item'),
+
+    // will be applied to the textfields
+    maskRe: undefined,
+
+    allowBlank: true,
+    selectAll: false,
+    isFormField: true,
+    deleteEmpty: false,
+    selModel: 'checkboxmodel',
+
+    config: {
+	deleteEmpty: false,
+    },
+
+    viewConfig: {
+	deferEmptyText: false,
+    },
+
+    setValue: function(list) {
+	let me = this;
+	list = Ext.isArray(list) ? list : (list ?? '').split(';');
+	if (list.length > 0) {
+	    me.getStore().setData(list.map(item => ({ item })));
+	} else {
+	    me.getStore().removeAll();
+	}
+	me.checkChange();
+	return me;
+    },
+
+    getValue: function() {
+	let me = this;
+	let values = [];
+	me.getStore().each((rec) => {
+	    if (rec.data.item) {
+		values.push(rec.data.item);
+	    }
+	});
+	return values.join(';');
+    },
+
+    getErrors: function(value) {
+	let me = this;
+	let empty = false;
+	me.getStore().each((rec) => {
+	    if (!rec.data.item) {
+		empty = true;
+	    }
+	});
+	if (empty) {
+	    return [gettext('Tag must not be empty.')];
+	}
+	return [];
+    },
+
+    // override framework function to implement deleteEmpty behaviour
+    getSubmitData: function() {
+	let me = this,
+	    data = null,
+	    val;
+	if (!me.disabled && me.submitValue) {
+	    val = me.getValue();
+	    if (val !== null && val !== '') {
+		data = {};
+		data[me.getName()] = val;
+	    } else if (me.getDeleteEmpty()) {
+		data = {};
+		data.delete = me.getName();
+	    }
+	}
+	return data;
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	addLine: function() {
+	    let me = this;
+	    me.getView().getStore().add({
+		item: '',
+	    });
+	},
+
+	removeSelection: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let selection = view.getSelection();
+	    if (selection === undefined) {
+		return;
+	    }
+
+	    selection.forEach((sel) => {
+		view.getStore().remove(sel);
+	    });
+	    view.checkChange();
+	},
+
+	itemChange: function(field, newValue) {
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+	    let column = field.getWidgetColumn();
+	    rec.set(column.dataIndex, newValue);
+	    field.up('pveListField').checkChange();
+	},
+    },
+
+    tbar: [
+	{
+	    text: gettext('Add'),
+	    handler: 'addLine',
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Remove'),
+	    handler: 'removeSelection',
+	    disabled: true,
+	},
+    ],
+
+    store: {
+	listeners: {
+	    update: function() {
+		this.commitChanges();
+	    },
+	},
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	me.columns = [
+	    {
+		header: me.fieldTtitle,
+		dataIndex: 'item',
+		xtype: 'widgetcolumn',
+		widget: {
+		    xtype: 'textfield',
+		    isFormField: false,
+		    maskRe: me.maskRe,
+		    allowBlank: false,
+		    queryMode: 'local',
+		    listeners: {
+			change: 'itemChange',
+		    },
+		},
+		flex: 1,
+	    },
+	];
+
+	me.callParent();
+	me.initField();
+    },
+});
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 07/12] ui: dc/OptionView: add editors for tag settings
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (13 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 06/12] ui: add PVE.form.ListField Dominik Csapak
@ 2022-11-14  9:43 ` Dominik Csapak
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 08/12] ui: add form/Tag Dominik Csapak
                   ` (5 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:43 UTC (permalink / raw)
  To: pve-devel

namely for 'tag-style' (shape & colors), 'user-tag-acces', and
'privileged tags'. display the tag overrides directly as they will appear
as tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes for v
 www/manager6/Utils.js         |  20 ++++
 www/manager6/dc/OptionView.js | 200 ++++++++++++++++++++++++++++++++++
 2 files changed, 220 insertions(+)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 2f8cdec77..f0748762c 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1923,6 +1923,26 @@ Ext.define('PVE.Utils', {
 	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
     },
 
+    tagTreeStyles: {
+	'__default__': Proxmox.Utils.defaultText,
+	'full': gettext('Full'),
+	'circle': gettext('Circle'),
+	'dense': gettext('Dense'),
+	'none': Proxmox.Utils.NoneText,
+    },
+
+    renderTags: function(tagstext, overrides) {
+	let text = '';
+	if (tagstext) {
+	    let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
+	    text += ' ';
+	    tags.forEach((tag) => {
+		text += Proxmox.Utils.getTagElement(tag, overrides);
+	    });
+	}
+	return text;
+    },
+
     tagCharRegex: /^[a-z0-9+_.-]$/i,
 },
 
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index ff96351d5..4b158107d 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -5,6 +5,7 @@ Ext.define('PVE.dc.OptionView', {
     onlineHelp: 'datacenter_configuration_file',
 
     monStoreErrors: true,
+    userCls: 'proxmox-tags-full',
 
     add_inputpanel_row: function(name, text, opts) {
 	var me = this;
@@ -312,6 +313,202 @@ Ext.define('PVE.dc.OptionView', {
 		submitValue: true,
 	    }],
 	});
+	me.rows['tag-style'] = {
+	    required: true,
+	    renderer: (value) => {
+		if (value === undefined) {
+		    return gettext('No Overrides');
+		}
+		let colors = PVE.Utils.parseTagOverrides(value?.['color-map']);
+		let shape = value.shape;
+		let shapeText = PVE.Utils.tagTreeStyles[shape] ?? Proxmox.Utils.defaultText;
+		let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText);
+		if (Object.keys(colors).length > 0) {
+		    txt += ', ';
+		}
+		for (const tag of Object.keys(colors)) {
+		    txt += Proxmox.Utils.getTagElement(tag, colors);
+		}
+		return txt;
+	    },
+	    header: gettext('Tag Style Override'),
+	    editor: {
+		xtype: 'proxmoxWindowEdit',
+		width: 800,
+		subject: gettext('Tag Color Override'),
+		onlineHelp: 'datacenter_configuration_file',
+		fieldDefaults: {
+		    labelWidth: 100,
+		},
+		url: '/api2/extjs/cluster/options',
+		items: [
+		    {
+			xtype: 'inputpanel',
+			setValues: function(values) {
+			    if (values === undefined) {
+				return undefined;
+			    }
+			    values = values?.['tag-style'] ?? {};
+			    values.shape = values.shape || '__default__';
+			    values.colors = values['color-map'];
+			    return Proxmox.panel.InputPanel.prototype.setValues.call(this, values);
+			},
+			onGetValues: function(values) {
+			    let style = {};
+			    if (values.colors) {
+				style['color-map'] = values.colors;
+			    }
+			    if (values.shape) {
+				style.shape = values.shape;
+			    }
+			    let value = PVE.Parser.printPropertyString(style);
+			    if (value === '') {
+				return {
+				    'delete': 'tag-style',
+				};
+			    }
+			    return {
+				'tag-style': value,
+			    };
+			},
+			items: [
+			    {
+				name: 'shape',
+				xtype: 'proxmoxKVComboBox',
+				fieldLabel: gettext('Tree Shape'),
+				comboItems: Object.entries(PVE.Utils.tagTreeStyles),
+				defaultValue: '__default__',
+				deleteEmpty: true,
+			    },
+			    {
+				xtype: 'displayfield',
+				fieldLabel: gettext('Color Overrides'),
+			    },
+			    {
+				name: 'colors',
+				xtype: 'pveTagColorGrid',
+				deleteEmpty: true,
+				height: 300,
+			    },
+			],
+		    },
+		],
+	    },
+	};
+
+	me.rows['user-tag-access'] = {
+	    required: true,
+	    renderer: (value) => {
+		if (value === undefined) {
+		    return Ext.String.format(gettext('Mode: {0}'), 'free');
+		}
+		let mode = value?.['user-allow'] ?? 'free';
+		let list = value?.['user-allow-list'].join(',');
+		let modeTxt = Ext.String.format(gettext('Mode {0}'), mode);
+		let overrides = PVE.Utils.tagOverrides;
+		let tags = PVE.Utils.renderTags(list, overrides);
+
+		return `${modeTxt}, ${gettext('Pre-defined:')} ${tags}`;
+	    },
+	    header: gettext('User Tag Access'),
+	    editor: {
+		xtype: 'proxmoxWindowEdit',
+		subject: gettext('User Tag Access'),
+		onlineHelp: 'datacenter_configuration_file',
+		url: '/api2/extjs/cluster/options',
+		items: [
+		    {
+			xtype: 'inputpanel',
+			setValues: function(values) {
+			    let data = values?.['user-tag-access'] ?? {};
+			    console.log(data);
+			    return Proxmox.panel.InputPanel.prototype.setValues.call(this, data);
+			},
+			onGetValues: function(values) {
+			    if (values === undefined || Object.keys(values).length === 0) {
+				return { 'delete': name };
+			    }
+			    return {
+				'user-tag-access': PVE.Parser.printPropertyString(values),
+			    };
+			},
+			items: [
+			    {
+				name: 'user-allow',
+				fieldLabel: gettext('Mode'),
+				xtype: 'proxmoxKVComboBox',
+				deleteEmpty: false,
+				value: '__default__',
+				comboItems: [
+				    ['__default__', Proxmox.Utils.defaultText + ' (free)'],
+				    ['free', 'free'],
+				    ['existing', 'existing'],
+				    ['list', 'list'],
+				    ['none', 'none'],
+				],
+				defaultValue: '__default__',
+			    },
+			    {
+				xtype: 'displayfield',
+				fieldLabel: gettext('Predefined Tags'),
+			    },
+			    {
+				name: 'user-allow-list',
+				xtype: 'pveListField',
+				emptyText: gettext('No Tags defined'),
+				fieldTitle: gettext('Tag'),
+				maskRe: PVE.Utils.tagCharRegex,
+				height: 200,
+				scrollable: true,
+			    },
+			],
+		    },
+		],
+	    },
+	};
+
+	me.rows['privileged-tags'] = {
+	    required: true,
+	    renderer: (value) => {
+		if (value === undefined) {
+		    return gettext('No Privilged Tags');
+		}
+		let overrides = PVE.Utils.tagOverrides;
+		return PVE.Utils.renderTags(value.join(','), overrides);
+	    },
+	    header: gettext('Privileged Tags'),
+	    editor: {
+		xtype: 'proxmoxWindowEdit',
+		subject: gettext('Privileged Tags'),
+		onlineHelp: 'datacenter_configuration_file',
+		url: '/api2/extjs/cluster/options',
+		items: [
+		    {
+			xtype: 'inputpanel',
+			setValues: function(values) {
+			    let tags = values?.['privileged-tags'] ?? '';
+			    return Proxmox.panel.InputPanel.prototype.setValues.call(this, { tags });
+			},
+			onGetValues: function(values) {
+			    return {
+				'privileged-tags': values,
+			    };
+			},
+			items: [
+			    {
+				name: 'tags',
+				xtype: 'pveListField',
+				emptyText: gettext('No Tags defined'),
+				fieldTitle: gettext('Tag'),
+				maskRe: PVE.Utils.tagCharRegex,
+				height: 200,
+				scrollable: true,
+			    },
+			],
+		    },
+		],
+	    },
+	};
 
 	me.selModel = Ext.create('Ext.selection.RowModel', {});
 
@@ -347,6 +544,9 @@ Ext.define('PVE.dc.OptionView', {
 	    if (rec.data.value === '__default__') {
 		delete PVE.UIOptions.console;
 	    }
+
+	    let tagStyle = store.getById('tag-style')?.data?.value;
+	    PVE.Utils.updateTagSettings(tagStyle?.['color-map'], tagStyle?.shape);
 	});
 
 	me.on('activate', me.rstore.startUpdate);
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 08/12] ui: add form/Tag
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (14 preceding siblings ...)
  2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 07/12] ui: dc/OptionView: add editors for tag settings Dominik Csapak
@ 2022-11-14  9:44 ` Dominik Csapak
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 09/12] ui: add form/TagEdit.js Dominik Csapak
                   ` (4 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:44 UTC (permalink / raw)
  To: pve-devel

displays a single tag, with the ability to edit inline on click (when
the mode is set to editable). This brings up a list of globally available tags
for simple selection.

this is a basic ext component, with 'i' tags for the icons (handle and
add/remove button) and a span (for the tag text)

shows the tag by default, and if put in editable mode, makes the
span editable. when actually starting the edit, shows a picker
with available tags to select from

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* added missing css classes
* added emulated textbox style (white-background+border) when editing
 www/css/ext6-pve.css     |  32 ++++++
 www/manager6/Makefile    |   1 +
 www/manager6/form/Tag.js | 233 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 266 insertions(+)
 create mode 100644 www/manager6/form/Tag.js

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index f7d0c4201..daaffa6ec 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -656,3 +656,35 @@ table.osds td:first-of-type {
     padding-top: 0px;
     padding-bottom: 0px;
 }
+
+.pve-edit-tag > i {
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.pve-edit-tag > i.handle {
+    padding-right: 5px;
+    cursor: grab;
+}
+
+.pve-edit-tag > i.action {
+    padding-left: 5px;
+}
+
+.pve-edit-tag.normal > i {
+    display: none;
+}
+
+.pve-edit-tag.editable span,
+.pve-edit-tag.inEdit span {
+    background-color: #ffffff;
+    border: 1px solid #a8a8a8;
+    color: #000;
+    padding-left: 2px;
+    padding-right: 2px;
+    min-width: 2em;
+}
+
+.pve-edit-tag.inEdit span {
+    border: 1px solid #000;
+}
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 9c7ced918..7bcc35e8e 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -75,6 +75,7 @@ JSSRC= 							\
 	form/iScsiProviderSelector.js			\
 	form/TagColorGrid.js				\
 	form/ListField.js				\
+	form/Tag.js					\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/form/Tag.js b/www/manager6/form/Tag.js
new file mode 100644
index 000000000..aa6ae8674
--- /dev/null
+++ b/www/manager6/form/Tag.js
@@ -0,0 +1,233 @@
+Ext.define('Proxmox.Tag', {
+    extend: 'Ext.Component',
+    alias: 'widget.pmxTag',
+
+    mode: 'editable',
+
+    icons: {
+	editable: 'fa fa-minus-square',
+	normal: '',
+	inEdit: 'fa fa-check-square',
+    },
+
+    tag: '',
+    cls: 'pve-edit-tag',
+
+    tpl: [
+	'<i class="handle fa fa-bars"></i>',
+	'<span>{tag}</span>',
+	'<i class="action {iconCls}"></i>',
+    ],
+
+    // we need to do this in mousedown, because that triggers before
+    // focusleave (which triggers before click)
+    onMouseDown: function(event) {
+	let me = this;
+	if (event.target.tagName !== 'I' || event.target.classList.contains('handle')) {
+	    return;
+	}
+	switch (me.mode) {
+	    case 'editable':
+		me.setVisible(false);
+		me.setTag('');
+		break;
+	    case 'inEdit':
+		me.setTag(me.tagEl().innerHTML);
+		me.setMode('editable');
+		break;
+	    default: break;
+	}
+    },
+
+    onClick: function(event) {
+	let me = this;
+	if (event.target.tagName !== 'SPAN' || me.mode !== 'editable') {
+	    return;
+	}
+	me.setMode('inEdit');
+
+	// select text in the element
+	let tagEl = me.tagEl();
+	tagEl.contentEditable = true;
+	let range = document.createRange();
+	range.selectNodeContents(tagEl);
+	let sel = window.getSelection();
+	sel.removeAllRanges();
+	sel.addRange(range);
+
+	me.showPicker();
+    },
+
+    showPicker: function() {
+	let me = this;
+	if (!me.picker) {
+	    me.picker = Ext.widget({
+		xtype: 'boundlist',
+		minWidth: 70,
+		scrollable: true,
+		floating: true,
+		hidden: true,
+		userCls: 'proxmox-tags-full',
+		displayField: 'tag',
+		itemTpl: [
+		    '{[Proxmox.Utils.getTagElement(values.tag, PVE.Utils.tagOverrides)]}',
+		],
+		store: [],
+		listeners: {
+		    select: function(picker, rec) {
+			me.setTag(rec.data.tag);
+			me.setMode('editable');
+			me.picker.hide();
+		    },
+		},
+	    });
+	}
+	me.picker.getStore()?.clearFilter();
+	let taglist = PVE.Utils.tagList.map(v => ({ tag: v }));
+	if (taglist.length < 1) {
+	    return;
+	}
+	me.picker.getStore().setData(taglist);
+	me.picker.showBy(me, 'tl-bl');
+	me.picker.setMaxHeight(200);
+    },
+
+    setMode: function(mode) {
+	let me = this;
+	if (me.icons[mode] === undefined) {
+	    throw "invalid mode";
+	}
+	let tagEl = me.tagEl();
+	if (tagEl) {
+	    tagEl.contentEditable = mode === 'inEdit';
+	}
+	me.removeCls(me.mode);
+	me.addCls(mode);
+	me.mode = mode;
+	me.updateData();
+    },
+
+    onKeyPress: function(event) {
+	let me = this;
+	let key = event.browserEvent.key;
+	switch (key) {
+	    case 'Enter':
+		if (me.tagEl().innerHTML !== '') {
+		    me.setTag(me.tagEl().innerHTML);
+		    me.setMode('editable');
+		    return;
+		}
+		break;
+	    case 'Escape':
+		me.cancelEdit();
+		return;
+	    case 'Backspace':
+	    case 'Delete':
+		return;
+	    default:
+		if (key.match(PVE.Utils.tagCharRegex)) {
+		    return;
+		}
+	}
+	event.browserEvent.preventDefault();
+	event.browserEvent.stopPropagation();
+    },
+
+    beforeInput: function(event) {
+	let me = this;
+	me.updateLayout();
+	let tag = event.event.data ?? event.event.dataTransfer?.getData('text/plain');
+	if (!tag) {
+	    return;
+	}
+	if (tag.match(PVE.Utils.tagCharRegex) === null) {
+	    event.event.preventDefault();
+	    event.event.stopPropagation();
+	}
+    },
+
+    onInput: function(event) {
+	let me = this;
+	me.picker.getStore().filter({
+	    property: 'tag',
+	    value: me.tagEl().innerHTML,
+	    anyMatch: true,
+	});
+    },
+
+    cancelEdit: function(list, event) {
+	let me = this;
+	if (me.mode === 'inEdit') {
+	    me.setTag(me.tag);
+	    me.setMode('editable');
+	}
+	me.picker?.hide();
+    },
+
+
+    setTag: function(tag) {
+	let me = this;
+	let oldtag = me.tag;
+	me.tag = tag;
+	let rgb = PVE.Utils.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag);
+
+	let cls = Proxmox.Utils.getTextContrastClass(rgb);
+	let color = Proxmox.Utils.rgbToCss(rgb);
+	me.setUserCls(`proxmox-tag-${cls}`);
+	me.setStyle('background-color', color);
+	if (rgb.length > 3) {
+	    let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
+
+	    me.setStyle('color', fgcolor);
+	} else {
+	    me.setStyle('color');
+	}
+	me.updateData();
+	if (oldtag !== tag) {
+	    me.fireEvent('change', me, tag, oldtag);
+	}
+    },
+
+    updateData: function() {
+	let me = this;
+	if (me.destroying || me.destroyed) {
+	    return;
+	}
+	me.update({
+	    tag: me.tag,
+	    iconCls: me.icons[me.mode],
+	});
+    },
+
+    tagEl: function() {
+	return this.el?.dom?.getElementsByTagName('span')?.[0];
+    },
+
+    listeners: {
+	mousedown: 'onMouseDown',
+	click: 'onClick',
+	focusleave: 'cancelEdit',
+	keydown: 'onKeyPress',
+	beforeInput: 'beforeInput',
+	input: 'onInput',
+	element: 'el',
+	scope: 'this',
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	me.setTag(me.tag);
+	me.setMode(me.mode ?? 'normal');
+	me.callParent();
+	me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => { me.setTag(me.tag); }); // refresh tag color
+    },
+
+    destroy: function() {
+	let me = this;
+	if (me.picker) {
+	    Ext.destroy(me.picker);
+	}
+	me.callParent();
+    },
+});
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 09/12] ui: add form/TagEdit.js
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (15 preceding siblings ...)
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 08/12] ui: add form/Tag Dominik Csapak
@ 2022-11-14  9:44 ` Dominik Csapak
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
                   ` (3 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:44 UTC (permalink / raw)
  To: pve-devel

this is a wrapper container for holding a list of (editable) tags
intended to be used in the lxc/qemu status toolbar

to add a new tag, we reuse the 'pmxTag' class, but overwrite some of
its behaviour and css classes so that it properly adds tags

also handles the drag/drop feature for the tags in the list

when done with editing (by clicking the checkmark), sends a 'change'
event with the new tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* added missing css classed
 www/css/ext6-pve.css         |  23 ++-
 www/manager6/Makefile        |   1 +
 www/manager6/form/TagEdit.js | 321 +++++++++++++++++++++++++++++++++++
 3 files changed, 341 insertions(+), 4 deletions(-)
 create mode 100644 www/manager6/form/TagEdit.js

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index daaffa6ec..4fc83a878 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -657,7 +657,8 @@ table.osds td:first-of-type {
     padding-bottom: 0px;
 }
 
-.pve-edit-tag > i {
+.pve-edit-tag > i,
+.pve-add-tag > i {
     cursor: pointer;
     font-size: 14px;
 }
@@ -667,7 +668,8 @@ table.osds td:first-of-type {
     cursor: grab;
 }
 
-.pve-edit-tag > i.action {
+.pve-edit-tag > i.action,
+.pve-add-tag > i.action {
     padding-left: 5px;
 }
 
@@ -676,7 +678,9 @@ table.osds td:first-of-type {
 }
 
 .pve-edit-tag.editable span,
-.pve-edit-tag.inEdit span {
+.pve-edit-tag.inEdit span,
+.pve-add-tag.editable span,
+.pve-add-tag.inEdit span {
     background-color: #ffffff;
     border: 1px solid #a8a8a8;
     color: #000;
@@ -685,6 +689,17 @@ table.osds td:first-of-type {
     min-width: 2em;
 }
 
-.pve-edit-tag.inEdit span {
+.pve-edit-tag.inEdit span,
+.pve-add-tag.inEdit span {
     border: 1px solid #000;
 }
+
+.pve-add-tag {
+    background-color: #d5d5d5 ! important;
+    color: #000000 ! important;
+}
+
+.pve-tag-inline-button {
+    cursor: pointer;
+    padding-left: 2px;
+}
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 7bcc35e8e..396abffcc 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -76,6 +76,7 @@ JSSRC= 							\
 	form/TagColorGrid.js				\
 	form/ListField.js				\
 	form/Tag.js					\
+	form/TagEdit.js					\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/form/TagEdit.js b/www/manager6/form/TagEdit.js
new file mode 100644
index 000000000..ac184a917
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,321 @@
+Ext.define('PVE.panel.TagEditContainer', {
+    extend: 'Ext.container.Container',
+    alias: 'widget.pveTagEditContainer',
+
+    layout: {
+	type: 'hbox',
+	align: 'stretch',
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	loadTags: function(tagstring = '', force = false) {
+	    let me = this;
+	    let view = me.getView();
+
+	    if (me.oldTags === tagstring && !force) {
+		return;
+	    }
+
+	    view.suspendLayout = true;
+	    me.forEachTag((tag) => {
+		view.remove(tag);
+	    });
+	    me.getViewModel().set('tagCount', 0);
+	    let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
+	    newtags.forEach((tag) => {
+		me.addTag(tag);
+	    });
+	    view.suspendLayout = false;
+	    view.updateLayout();
+	    if (!force) {
+		me.oldTags = tagstring;
+	    }
+	},
+
+	onRender: function(v) {
+	    let me = this;
+	    let view = me.getView();
+	    view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
+		getDragData: function(e) {
+		    let source = e.getTarget('.handle');
+		    if (!source) {
+			return undefined;
+		    }
+		    let sourceId = source.parentNode.id;
+		    let cmp = Ext.getCmp(sourceId);
+		    let ddel = document.createElement('div');
+		    ddel.classList.add('proxmox-tags-full');
+		    ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.Utils.tagOverrides);
+		    let repairXY = Ext.fly(source).getXY();
+		    cmp.setDisabled(true);
+		    ddel.id = Ext.id();
+		    return {
+			ddel,
+			repairXY,
+			sourceId,
+		    };
+		},
+		onMouseUp: function(target, e, id) {
+		    let cmp = Ext.getCmp(this.dragData.sourceId);
+		    if (cmp && !cmp.isDestroyed) {
+			cmp.setDisabled(false);
+		    }
+		},
+		getRepairXY: function() {
+		    return this.dragData.repairXY;
+		},
+		beforeInvalidDrop: function(target, e, id) {
+		    let cmp = Ext.getCmp(this.dragData.sourceId);
+		    if (cmp && !cmp.isDestroyed) {
+			cmp.setDisabled(false);
+		    }
+		},
+	    });
+	    view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), {
+		getTargetFromEvent: function(e) {
+		    return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light');
+		},
+		getIndicator: function() {
+		    if (!view.indicator) {
+			view.indicator = Ext.create('Ext.Component', {
+			    floating: true,
+			    html: '<i class="fa fa-long-arrow-up"></i>',
+			    hidden: true,
+			    shadow: false,
+			});
+		    }
+		    return view.indicator;
+		},
+		onContainerOver: function() {
+		    this.getIndicator().setVisible(false);
+		},
+		notifyOut: function() {
+		    this.getIndicator().setVisible(false);
+		},
+		onNodeOver: function(target, dd, e, data) {
+		    let indicator = this.getIndicator();
+		    indicator.setVisible(true);
+		    indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]);
+		    return this.dropAllowed;
+		},
+		onNodeDrop: function(target, dd, e, data) {
+		    this.getIndicator().setVisible(false);
+		    let sourceCmp = Ext.getCmp(data.sourceId);
+		    if (!sourceCmp) {
+			return;
+		    }
+		    sourceCmp.setDisabled(false);
+		    let targetCmp = Ext.getCmp(target.id);
+		    view.remove(sourceCmp, { destroy: false });
+		    view.insert(view.items.indexOf(targetCmp), sourceCmp);
+		},
+	    });
+	},
+
+	forEachTag: function(func) {
+	    let me = this;
+	    let view = me.getView();
+	    view.items.each((field) => {
+		if (field.reference === 'addTagBtn') {
+		    return false;
+		}
+		if (field.getXType() === 'pmxTag') {
+		    func(field);
+		}
+		return true;
+	    });
+	},
+
+	toggleEdit: function(cancel) {
+	    let me = this;
+	    let vm = me.getViewModel();
+	    let editMode = !vm.get('editMode');
+	    vm.set('editMode', editMode);
+
+	    // get a current tag list for editing
+	    if (editMode) {
+		PVE.Utils.updateUIOptions();
+	    }
+
+	    me.forEachTag((tag) => {
+		tag.setMode(editMode ? 'editable' : 'normal');
+	    });
+
+	    if (!vm.get('editMode')) {
+		let tags = [];
+		if (cancel) {
+		    me.loadTags(me.oldTags, true);
+		} else {
+		    me.forEachTag((cmp) => {
+			if (cmp.isVisible() && cmp.tag) {
+			    tags.push(cmp.tag);
+			}
+		    });
+		    tags = tags.join(',');
+		    if (me.oldTags !== tags) {
+			me.oldTags = tags;
+			me.getView().fireEvent('change', tags);
+		    }
+		}
+	    }
+	    me.getView().updateLayout();
+	},
+
+	addTag: function(tag) {
+	    let me = this;
+	    let view = me.getView();
+	    let vm = me.getViewModel();
+	    let index = view.items.indexOf(me.lookup('addTagBtn'));
+	    view.insert(index, {
+		xtype: 'pmxTag',
+		tag,
+		mode: vm.get('editMode') ? 'editable' : 'normal',
+		listeners: {
+		    change: (field, newTag) => {
+			if (newTag === '') {
+			    view.remove(field);
+			    vm.set('tagCount', vm.get('tagCount') - 1);
+			}
+		    },
+		},
+	    });
+
+	    vm.set('tagCount', vm.get('tagCount') + 1);
+	},
+
+	addTagClick: function(event) {
+	    let me = this;
+	    if (event.target.tagName === 'SPAN') {
+		me.lookup('addTagBtn').tagEl().innerHTML = '';
+		me.lookup('addTagBtn').updateLayout();
+	    }
+	},
+
+	addTagMouseDown: function(event) {
+	    let me = this;
+	    if (event.target.tagName === 'I') {
+		let tag = me.lookup('addTagBtn').tagEl().innerHTML;
+		if (tag !== '') {
+		    me.addTag(tag, true);
+		}
+	    }
+	},
+
+	addTagChange: function(field, tag) {
+	    let me = this;
+	    if (tag !== '') {
+		me.addTag(tag, true);
+	    }
+	    field.tag = '';
+	},
+
+	cancelClick: function() {
+	    this.toggleEdit(true);
+	},
+
+	editClick: function() {
+	    this.toggleEdit(false);
+	},
+
+	init: function(view) {
+	    let me = this;
+	    if (view.tags) {
+		me.loadTags(view.tags);
+	    }
+	},
+    },
+
+    viewModel: {
+	data: {
+	    tagCount: 0,
+	    editMode: false,
+	},
+
+	formulas: {
+	    hideNoTags: function(get) {
+		return get('editMode') || get('tagCount') !== 0;
+	    },
+	    editBtnHtml: function(get) {
+		let cls = get('editMode') ? 'check' : 'pencil';
+		let qtip = get('editMode') ? gettext('Apply Changes') : gettext('Edit Tags');
+		return `<i data-qtip="${qtip}" class="fa fa-${cls}"></i>`;
+	    },
+	},
+    },
+
+    loadTags: function() {
+	return this.getController().loadTags(...arguments);
+    },
+
+    items: [
+	{
+	    xtype: 'box',
+	    bind: {
+		hidden: '{hideNoTags}',
+	    },
+	    html: gettext('No Tags'),
+	},
+	{
+	    xtype: 'pmxTag',
+	    reference: 'addTagBtn',
+	    cls: 'pve-add-tag',
+	    mode: 'editable',
+	    tag: '',
+	    tpl: `<span>${gettext('Add Tag')}</span><i class="action fa fa-plus-square"></i>`,
+	    bind: {
+		hidden: '{!editMode}',
+	    },
+	    hidden: true,
+	    onMouseDown: Ext.emptyFn, // prevent default behaviour
+	    listeners: {
+		click: {
+		    element: 'el',
+		    fn: 'addTagClick',
+		},
+		mousedown: {
+		    element: 'el',
+		    fn: 'addTagMouseDown',
+		},
+		change: 'addTagChange',
+	    },
+	},
+	{
+	    xtype: 'box',
+	    html: `<i data-qtip="${gettext('Cancel')}" class="fa fa-times"></i>`,
+	    cls: 'pve-tag-inline-button',
+	    hidden: true,
+	    bind: {
+		hidden: '{!editMode}',
+	    },
+	    listeners: {
+		click: 'cancelClick',
+		element: 'el',
+	    },
+	},
+	{
+	    xtype: 'box',
+	    cls: 'pve-tag-inline-button',
+	    bind: {
+		html: '{editBtnHtml}',
+	    },
+	    listeners: {
+		click: 'editClick',
+		element: 'el',
+	    },
+	},
+    ],
+
+    listeners: {
+	render: 'onRender',
+    },
+
+    destroy: function() {
+	let me = this;
+	Ext.destroy(me.dragzone);
+	Ext.destroy(me.dropzone);
+	Ext.destroy(me.indicator);
+	me.callParent();
+    },
+});
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (16 preceding siblings ...)
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 09/12] ui: add form/TagEdit.js Dominik Csapak
@ 2022-11-14  9:44 ` Dominik Csapak
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 11/12] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
                   ` (2 subsequent siblings)
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:44 UTC (permalink / raw)
  To: pve-devel

add the tags in the status line, and add a button for adding new ones

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/lxc/Config.js  | 32 ++++++++++++++++++++++++++++++--
 www/manager6/qemu/Config.js | 31 +++++++++++++++++++++++++++++--
 2 files changed, 59 insertions(+), 4 deletions(-)

diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js
index 93f385db4..9b3017add 100644
--- a/www/manager6/lxc/Config.js
+++ b/www/manager6/lxc/Config.js
@@ -4,6 +4,8 @@ Ext.define('PVE.lxc.Config', {
 
     onlineHelp: 'chapter_pct',
 
+    userCls: 'proxmox-tags-full',
+
     initComponent: function() {
         var me = this;
 	var vm = me.pveSelNode.data;
@@ -182,12 +184,33 @@ Ext.define('PVE.lxc.Config', {
 	    ],
 	});
 
+	let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+	    tags: vm.tags,
+	    listeners: {
+		change: function(tags) {
+		    Proxmox.Utils.API2Request({
+			url: base_url + '/config',
+			method: 'PUT',
+			params: {
+			    tags,
+			},
+			success: function() {
+			    me.statusStore.load();
+			},
+			failure: function(response) {
+			    Ext.Msg.alert('Error', response.htmlStatus);
+			    me.statusStore.load();
+			},
+		    });
+		},
+	    },
+	});
 
 	Ext.apply(me, {
 	    title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename),
 	    hstateid: 'lxctab',
 	    tbarSpacing: false,
-	    tbar: [statusTxt, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+	    tbar: [statusTxt, tagsContainer, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
 	    defaults: { statusStore: me.statusStore },
 	    items: [
 		{
@@ -344,10 +367,12 @@ Ext.define('PVE.lxc.Config', {
 	me.mon(me.statusStore, 'load', function(s, records, success) {
 	    var status;
 	    var lock;
+	    var rec;
+
 	    if (!success) {
 		status = 'unknown';
 	    } else {
-		var rec = s.data.get('status');
+		rec = s.data.get('status');
 		status = rec ? rec.data.value : 'unknown';
 		rec = s.data.get('template');
 		template = rec ? rec.data.value : false;
@@ -357,6 +382,9 @@ Ext.define('PVE.lxc.Config', {
 
 	    statusTxt.update({ lock: lock });
 
+	    rec = s.data.get('tags');
+	    tagsContainer.loadTags(rec?.data?.value);
+
 	    startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
 	    shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
 	    me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index 9fe933dfc..2cd6d8567 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -3,6 +3,7 @@ Ext.define('PVE.qemu.Config', {
     alias: 'widget.PVE.qemu.Config',
 
     onlineHelp: 'chapter_virtual_machines',
+    userCls: 'proxmox-tags-full',
 
     initComponent: function() {
         var me = this;
@@ -219,11 +220,33 @@ Ext.define('PVE.qemu.Config', {
 	    ],
 	});
 
+	let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+	    tags: vm.tags,
+	    listeners: {
+		change: function(tags) {
+		    Proxmox.Utils.API2Request({
+			url: base_url + '/config',
+			method: 'PUT',
+			params: {
+			    tags,
+			},
+			success: function() {
+			    me.statusStore.load();
+			},
+			failure: function(response) {
+			    Ext.Msg.alert('Error', response.htmlStatus);
+			    me.statusStore.load();
+			},
+		    });
+		},
+	    },
+	});
+
 	Ext.apply(me, {
 	    title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename),
 	    hstateid: 'kvmtab',
 	    tbarSpacing: false,
-	    tbar: [statusTxt, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+	    tbar: [statusTxt, tagsContainer, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
 	    defaults: { statusStore: me.statusStore },
 	    items: [
 		{
@@ -382,11 +405,12 @@ Ext.define('PVE.qemu.Config', {
 	    var spice = false;
 	    var xtermjs = false;
 	    var lock;
+	    var rec;
 
 	    if (!success) {
 		status = qmpstatus = 'unknown';
 	    } else {
-		var rec = s.data.get('status');
+		rec = s.data.get('status');
 		status = rec ? rec.data.value : 'unknown';
 		rec = s.data.get('qmpstatus');
 		qmpstatus = rec ? rec.data.value : 'unknown';
@@ -399,6 +423,9 @@ Ext.define('PVE.qemu.Config', {
 		xtermjs = !!s.data.get('serial');
 	    }
 
+	    rec = s.data.get('tags');
+	    tagsContainer.loadTags(rec?.data?.value);
+
 	    if (template) {
 		return;
 	    }
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 11/12] ui: tree/ResourceTree: show Tags in tree
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (17 preceding siblings ...)
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
@ 2022-11-14  9:44 ` Dominik Csapak
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 12/12] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
  2022-11-14 17:20 ` [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Aaron Lauterer
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:44 UTC (permalink / raw)
  To: pve-devel

and update the treenodes when the tags change.
since we change the vm node text (which we pass through to the config
panel), we have to change how we generate the text there slightly
(otherwise that would include the rendered tags)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* merged the updating logic here, since we don't collect the tags from
  the tree anymore
* improve commit message
 www/manager6/data/ResourceStore.js |  6 ++++++
 www/manager6/lxc/Config.js         |  4 +++-
 www/manager6/qemu/Config.js        |  4 +++-
 www/manager6/tree/ResourceTree.js  | 10 +++++++++-
 4 files changed, 21 insertions(+), 3 deletions(-)

diff --git a/www/manager6/data/ResourceStore.js b/www/manager6/data/ResourceStore.js
index c7b723060..b18f7dd8d 100644
--- a/www/manager6/data/ResourceStore.js
+++ b/www/manager6/data/ResourceStore.js
@@ -293,6 +293,12 @@ Ext.define('PVE.data.ResourceStore', {
 		sortable: true,
 		width: 100,
 	    },
+	    tags: {
+		header: gettext('Tags'),
+		type: 'string',
+		hidden: true,
+		sortable: true,
+	    },
 	};
 
 	let fields = [];
diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js
index 9b3017add..f33390513 100644
--- a/www/manager6/lxc/Config.js
+++ b/www/manager6/lxc/Config.js
@@ -206,8 +206,10 @@ Ext.define('PVE.lxc.Config', {
 	    },
 	});
 
+	let vm_text = `${vm.vmid} (${vm.name})`;
+
 	Ext.apply(me, {
-	    title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename),
+	    title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm_text, nodename),
 	    hstateid: 'lxctab',
 	    tbarSpacing: false,
 	    tbar: [statusTxt, tagsContainer, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index 2cd6d8567..5c8fa620d 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -242,8 +242,10 @@ Ext.define('PVE.qemu.Config', {
 	    },
 	});
 
+	let vm_text = `${vm.vmid} (${vm.name})`;
+
 	Ext.apply(me, {
-	    title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename),
+	    title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm_text, nodename),
 	    hstateid: 'kvmtab',
 	    tbarSpacing: false,
 	    tbar: [statusTxt, tagsContainer, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js
index be90d4f7a..5c92d4128 100644
--- a/www/manager6/tree/ResourceTree.js
+++ b/www/manager6/tree/ResourceTree.js
@@ -5,6 +5,8 @@ Ext.define('PVE.tree.ResourceTree', {
     extend: 'Ext.tree.TreePanel',
     alias: ['widget.pveResourceTree'],
 
+    userCls: 'proxmox-tags-circle',
+
     statics: {
 	typeDefaults: {
 	    node: {
@@ -114,6 +116,8 @@ Ext.define('PVE.tree.ResourceTree', {
 	    }
 	}
 
+	info.text += PVE.Utils.renderTags(info.tags, PVE.Utils.tagOverrides);
+
 	info.text = status + info.text;
     },
 
@@ -226,6 +230,10 @@ Ext.define('PVE.tree.ResourceTree', {
 
 	let stateid = 'rid';
 
+	const changedFields = [
+	    'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags',
+	];
+
 	let updateTree = function() {
 	    store.suspendEvents();
 
@@ -261,7 +269,7 @@ Ext.define('PVE.tree.ResourceTree', {
 		    }
 
 		    // tree item has been updated
-		    for (const field of ['text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock']) {
+		    for (const field of changedFields) {
 			if (item.data[field] !== olditem.data[field]) {
 			    changed = true;
 			    break;
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* [pve-devel] [PATCH manager v9 12/12] ui: add tags to ResourceGrid and GlobalSearchField
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (18 preceding siblings ...)
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 11/12] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
@ 2022-11-14  9:44 ` Dominik Csapak
  2022-11-14 17:20 ` [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Aaron Lauterer
  20 siblings, 0 replies; 27+ messages in thread
From: Dominik Csapak @ 2022-11-14  9:44 UTC (permalink / raw)
  To: pve-devel

also allows to search for tags in the GlobalSearchField where each tag is
treated like a seperate field, so it weighs more if the user searches for
the exact string of a single tag

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>

ui: ResourceGrid: render tags

with the 'full' styling

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/data/ResourceStore.js     |  1 +
 www/manager6/form/GlobalSearchField.js | 20 +++++++++++++++-----
 www/manager6/grid/ResourceGrid.js      |  1 +
 3 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/www/manager6/data/ResourceStore.js b/www/manager6/data/ResourceStore.js
index b18f7dd8d..ed1f4699e 100644
--- a/www/manager6/data/ResourceStore.js
+++ b/www/manager6/data/ResourceStore.js
@@ -295,6 +295,7 @@ Ext.define('PVE.data.ResourceStore', {
 	    },
 	    tags: {
 		header: gettext('Tags'),
+		renderer: (value) => PVE.Utils.renderTags(value, PVE.Utils.tagOverrides),
 		type: 'string',
 		hidden: true,
 		sortable: true,
diff --git a/www/manager6/form/GlobalSearchField.js b/www/manager6/form/GlobalSearchField.js
index 267a480d7..8e815d4f5 100644
--- a/www/manager6/form/GlobalSearchField.js
+++ b/www/manager6/form/GlobalSearchField.js
@@ -15,6 +15,7 @@ Ext.define('PVE.form.GlobalSearchField', {
 
     grid: {
 	xtype: 'gridpanel',
+	userCls: 'proxmox-tags-full',
 	focusOnToFront: false,
 	floating: true,
 	emptyText: Proxmox.Utils.noneText,
@@ -23,7 +24,7 @@ Ext.define('PVE.form.GlobalSearchField', {
 	scrollable: {
 	    xtype: 'scroller',
 	    y: true,
-	    x: false,
+	    x: true,
 	},
 	store: {
 	    model: 'PVEResources',
@@ -78,6 +79,11 @@ Ext.define('PVE.form.GlobalSearchField', {
 		text: gettext('Description'),
 		flex: 1,
 		dataIndex: 'text',
+		renderer: function(value, mD, rec) {
+		    let overrides = PVE.Utils.tagOverrides;
+		    let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
+		    return `${value}${tags}`;
+		},
 	    },
 	    {
 		text: gettext('Node'),
@@ -104,16 +110,20 @@ Ext.define('PVE.form.GlobalSearchField', {
 	    'storage': ['type', 'pool', 'node', 'storage'],
 	    'default': ['name', 'type', 'node', 'pool', 'vmid'],
 	};
-	let fieldArr = fieldMap[item.data.type] || fieldMap.default;
+	let fields = fieldMap[item.data.type] || fieldMap.default;
+	let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase());
+	if (item.data.tags) {
+	    let tags = item.data.tags.split(/[;, ]/);
+	    fieldArr.push(...tags);
+	}
 
 	let filterWords = me.filterVal.split(/\s+/);
 
 	// all text is case insensitive and each split-out word is searched for separately.
 	// a row gets 1 point for every partial match, and and additional point for every exact match
 	let match = 0;
-	for (let field of fieldArr) {
-	    let fieldValue = item.data[field]?.toString().toLowerCase();
-	    if (fieldValue === undefined) {
+	for (let fieldValue of fieldArr) {
+	    if (fieldValue === undefined || fieldValue === "") {
 		continue;
 	    }
 	    for (let filterWord of filterWords) {
diff --git a/www/manager6/grid/ResourceGrid.js b/www/manager6/grid/ResourceGrid.js
index 29906a376..9376bcc26 100644
--- a/www/manager6/grid/ResourceGrid.js
+++ b/www/manager6/grid/ResourceGrid.js
@@ -7,6 +7,7 @@ Ext.define('PVE.grid.ResourceGrid', {
 	property: 'type',
 	direction: 'ASC',
     },
+    userCls: 'proxmox-tags-full',
     initComponent: function() {
 	let me = this;
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 27+ messages in thread

* Re: [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-11-14 13:15   ` Wolfgang Bumiller
  0 siblings, 0 replies; 27+ messages in thread
From: Wolfgang Bumiller @ 2022-11-14 13:15 UTC (permalink / raw)
  To: Dominik Csapak; +Cc: pve-devel

On Mon, Nov 14, 2022 at 10:43:45AM +0100, Dominik Csapak wrote:
> for getting multiple properties from the in memory config of the
> guests in one go. Keep the existing IPC call as is for backward
> compatibility and add this as separate, new one.
> 
> It basically behaves the same as
> CFS_IPC_GET_GUEST_CONFIG_PROPERTY, but takes a list of properties
> instead and returns multiple properties per guest.
> 
> The old way of getting a single property is now also done by
> the new function, since it basically performs identically.
> 
> Here a short benchmark:
> 
> Setup:
> PVE in a VM with cpu type host (12700k) and 4 cores
> 10000 typical configs with both 'lock' and 'tags' set at the end
> and fairly long tags ('test-tag1,test-tag2,test-tag3')
> (normal vm with a snapshot, ~1KiB)
> 
> i let it run 100 times each, times in ms
> 
> old code:
> 
> total time  time per iteration
> 1054.2      10.2
> 
> new code:
> 
> num props  total time  time per iter  function
> 2          1332.2      13.2           get_properties
> 1          1051.2      10.2           get_properties
> 2          2082.2      20.2           get_property (2 calls to get_property)
> 1          1034.2      10.2           get_property
> 
> so a call with the new code for one property is the same as with the
> old code, and adding a second property only adds a bit of additional
> time (in this case ~30%)
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
>  data/src/cfs-ipc-ops.h |   2 +
>  data/src/server.c      |  64 +++++++++++++++
>  data/src/status.c      | 177 ++++++++++++++++++++++++++++-------------
>  data/src/status.h      |   3 +
>  4 files changed, 190 insertions(+), 56 deletions(-)
> 
> diff --git a/data/src/cfs-ipc-ops.h b/data/src/cfs-ipc-ops.h
> index 003e233..249308d 100644
> --- a/data/src/cfs-ipc-ops.h
> +++ b/data/src/cfs-ipc-ops.h
> @@ -43,4 +43,6 @@
>  
>  #define CFS_IPC_VERIFY_TOKEN 12
>  
> +#define CFS_IPC_GET_GUEST_CONFIG_PROPERTIES 13
> +
>  #endif
> diff --git a/data/src/server.c b/data/src/server.c
> index 549788a..ced9cfc 100644
> --- a/data/src/server.c
> +++ b/data/src/server.c
> @@ -89,6 +89,13 @@ typedef struct {
>  	char property[];
>  } cfs_guest_config_propery_get_request_header_t;
>  
> +typedef struct {
> +	struct qb_ipc_request_header req_header;
> +	uint32_t vmid;
> +	uint8_t num_props;
> +	char props[]; /* list of \0 terminated properties */
> +} cfs_guest_config_properties_get_request_header_t;
> +
>  typedef struct {
>  	struct qb_ipc_request_header req_header;
>  	char token[];
> @@ -348,6 +355,63 @@ static int32_t s1_msg_process_fn(
>  
>  			result = cfs_create_guest_conf_property_msg(outbuf, memdb, rh->property, rh->vmid);
>  		}
> +	} else if (request_id == CFS_IPC_GET_GUEST_CONFIG_PROPERTIES) {
> +
> +		cfs_guest_config_properties_get_request_header_t *rh =
> +			(cfs_guest_config_properties_get_request_header_t *) data;
> +
> +		size_t remaining = request_size - G_STRUCT_OFFSET(cfs_guest_config_properties_get_request_header_t, props);
> +
> +		result = 0;
> +		if (rh->vmid < 100 && rh->vmid != 0) {
> +			cfs_debug("vmid out of range %u", rh->vmid);
> +			result = -EINVAL;
> +		} else if (rh->vmid >= 100 && !vmlist_vm_exists(rh->vmid)) {
> +			result = -ENOENT;
> +		} else if (rh->num_props == 0) {
> +			cfs_debug("num_props == 0");
> +			result = -EINVAL;
> +		} else if (remaining <= 1) {
> +			cfs_debug("property length <= 1, %ld", remaining);
> +			result = -EINVAL;
> +		} else {
> +			const char **properties = malloc(sizeof(char*) * rh->num_props);
> +			char *current = (rh->props);
> +			for (uint8_t i = 0; i < rh->num_props; i++) {
> +			    size_t proplen = strnlen(current, remaining);
> +			    if (proplen == 0) {
> +				cfs_debug("property length 0");
> +				result = -EINVAL;
> +				break;
> +			    }
> +			    if (proplen == remaining) {
> +				cfs_debug("property not \\0 terminated");
> +				result = -EINVAL;
> +				break;
> +			    }
> +			    if (current[0] < 'a' || current[0] > 'z') {
> +				cfs_debug("property does not start with [a-z]");
> +				result = -EINVAL;
> +				break;
> +			    }
> +			    properties[i] = current;
> +			    current[proplen] = '\0'; // ensure property is 0 terminated

^ this is useless since you're literally splitting at \0. If you feel
unsure, include `|| current[proplen]` in the `proplen == remaining`
check.

> +			    remaining -= (proplen + 1);
> +			    current += proplen + 1;
> +			}
> +
> +			if (remaining != 0) {
> +			    cfs_debug("leftover data after parsing %u properties", rh->num_props);
> +			    result = -EINVAL;
> +			}
> +
> +			if (result == 0) {
> +			    cfs_debug("cfs_get_guest_config_properties: basic valid checked, do request");

*validity

> +			    result = cfs_create_guest_conf_properties_msg(outbuf, memdb, properties, rh->num_props, rh->vmid);
> +			}
> +
> +			free(properties);
> +		}
>  	} else if (request_id == CFS_IPC_VERIFY_TOKEN) {
>  
>  		cfs_verify_token_request_header_t *rh = (cfs_verify_token_request_header_t *) data;
> diff --git a/data/src/status.c b/data/src/status.c
> index 9bceaeb..e2bedaa 100644
> --- a/data/src/status.c
> +++ b/data/src/status.c
> @@ -804,25 +804,52 @@ cfs_create_vmlist_msg(GString *str)
>  	return 0;
>  }
>  
> -// checks the conf for a line starting with '$prop:' and returns the value
> -// afterwards, whitout initial whitespace(s), we only deal with the format
> -// restricion imposed by our perl VM config parser, main reference is
> +// checks if a config line starts with the given prop. if yes, writes a '\0'
> +// at the end of the value, and returns the pointer where the value starts
> +char *
> +_get_property_value_from_line(char *line, int line_len, const char *prop, int prop_len)

line_len and prop_len should be size_t
and yes, that change may lead to fixing up currently-wrong types and
casting those pesky wrong high-level fuse integer types...
(`int read`, that's just painful)

> +{
> +	if (line_len <= prop_len + 1) return NULL;
> +
> +	if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
> +		char *v_start = &line[prop_len + 1];
> +
> +		// drop initial value whitespaces here already
> +		while (*v_start && isspace(*v_start)) v_start++;

^ this on its own might dereference bytes after the line
add a `line_end = line + line_len` and start with `v_start != line_end &&`.

> +
> +		if (!*v_start) return NULL;

and then
v_start == line_end || !*v_start

> +
> +		char *v_end = &line[line_len - 1];
> +		while (v_end > v_start && isspace(*v_end)) v_end--;
> +		v_end[1] = '\0';
> +
> +		return v_start;
> +	}
> +
> +	return NULL;
> +}
> +
> +// checks the conf for lines starting with the given props and
> +// writes the pointers into the correct positions into the 'found' array
> +// afterwards, without initial whitespace(s), we only deal with the format
> +// restriction imposed by our perl VM config parser, main reference is
>  // PVE::QemuServer::parse_vm_config this allows to be very fast and still
>  // relatively simple
> -// main restrictions used for our advantage is the properties match reges:
> +// main restrictions used for our advantage is the properties match regex:
>  // ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) from parse_vm_config
>  // currently we only look at the current configuration in place, i.e., *no*
> -// snapshort and *no* pending changes
> -static char *
> -_get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
> +// snapshot and *no* pending changes
> +void
> +_get_property_values(char **found, char *conf, int conf_size, const char **props, uint8_t num_props, char min, char max)

^ min/max should be documented, is it even really worth it?

>  {
>  	const char *const conf_end = conf + conf_size;
>  	char *line = conf;
> -	size_t remaining_size;
> +	size_t remaining_size = conf_size;
> +	int count = 0;
>  
>  	char *next_newline = memchr(conf, '\n', conf_size);
>  	if (next_newline == NULL) {
> -		return NULL; // valid property lines end with \n, but none in the config
> +		return; // valid property lines end with \n, but none in the config
>  	}
>  	*next_newline = '\0';
>  
> @@ -830,41 +857,32 @@ _get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
>  		if (!line[0]) goto next;
>  
>  		// snapshot or pending section start, but nothing found yet -> not found
> -		if (line[0] == '[') return NULL;
> -		// properties start with /^[a-z]/, so continue early if not
> -		if (line[0] < 'a' || line[0] > 'z') goto next;
> +		if (line[0] == '[') return;
> +		// continue early if line does not begin with the min/max char of the properties
> +		if (line[0] < min || line[0] > max) goto next;
>  
>  		int line_len = strlen(line);
> -		if (line_len <= prop_len + 1) goto next;
> -
> -		if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
> -			char *v_start = &line[prop_len + 1];
> -
> -			// drop initial value whitespaces here already
> -			while (*v_start && isspace(*v_start)) v_start++;
> -
> -			if (!*v_start) return NULL;
> -
> -			char *v_end = &line[line_len - 1];
> -			while (v_end > v_start && isspace(*v_end)) v_end--;
> -			v_end[1] = '\0';
> -
> -			return v_start;
> +		for (uint8_t i = 0; i < num_props; i++) {
> +			char * value = _get_property_value_from_line(line, line_len, props[i], strlen(props[i]));
> +			if (value != NULL) {
> +				count += (found[i] != NULL) & 0x1; // count newly found lines
> +				found[i] = value;
> +			}
> +		}
> +		if (count == num_props) {
> +			return; // found all
>  		}
>  next:
>  		line = next_newline + 1;
>  		remaining_size = conf_end - line;
> -		if (remaining_size <= prop_len) {
> -			return NULL;
> -		}
>  		next_newline = memchr(line, '\n', remaining_size);
>  		if (next_newline == NULL) {
> -			return NULL; // valid property lines end with \n, but none in the config
> +			return; // valid property lines end with \n, but none in the config
>  		}
>  		*next_newline = '\0';
>  	}
>  
> -	return NULL; // not found
> +	return;
>  }
>  
>  static void
> @@ -883,24 +901,77 @@ _g_str_append_kv_jsonescaped(GString *str, const char *k, const char *v)
>  }
>  
>  int
> -cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
> +_print_found_properties(
> +	GString *str,
> +	gpointer conf,
> +	int size,
> +	const char **props,
> +	uint8_t num_props,
> +	uint32_t vmid,
> +	char **values,
> +	char min,
> +	char max,
> +	int first)
> +{
> +	_get_property_values(values, conf, size, props, num_props, min, max);
> +
> +	uint8_t found = 0;
> +	for (uint8_t i = 0; i < num_props; i++) {
> +		if (values[i] == NULL) {
> +			continue;
> +		}
> +		if (found) {
> +			g_string_append_c(str, ',');
> +		} else {
> +			if (!first) {
> +				g_string_append_printf(str, ",\n");
> +			} else {
> +				first = 0;
> +			}
> +			g_string_append_printf(str, "\"%u\":{", vmid);
> +			found = 1;
> +		}
> +		_g_str_append_kv_jsonescaped(str, props[i], values[i]);
> +	}
> +
> +	if (found) {
> +		g_string_append_c(str, '}');
> +	}
> +
> +	return first;
> +}
> +
> +int
> +cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid)
>  {
>  	g_return_val_if_fail(cfs_status.vmlist != NULL, -EINVAL);
>  	g_return_val_if_fail(str != NULL, -EINVAL);
>  
> -	int prop_len = strlen(prop);
> -	int res = 0;
> -	GString *path = NULL;
> -
>  	// Prelock &memdb->mutex in order to enable the usage of memdb_read_nolock
>  	// to prevent Deadlocks as in #2553
>  	g_mutex_lock (&memdb->mutex);
>  	g_mutex_lock (&mutex);
>  
> -	g_string_printf(str,"{\n");
> +	g_string_printf(str, "{\n");
>  
>  	GHashTable *ht = cfs_status.vmlist;
> +
> +	int res = 0;
> +	GString *path = NULL;
>  	gpointer tmp = NULL;
> +	char **values = calloc(num_props, sizeof(char*));
> +	char min = 'z', max = 'a';
> +
> +	for (uint8_t i = 0; i < num_props; i++) {
> +		if (props[i][0] > max) {
> +			max = props[i][0];
> +		}
> +
> +		if (props[i][0] < min) {
> +			min = props[i][0];
> +		}
> +	}
> +
>  	if (!g_hash_table_size(ht)) {
>  		goto ret;
>  	}
> @@ -919,15 +990,8 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
>  		// use memdb_read_nolock because lock is handled here
>  		int size = memdb_read_nolock(memdb, path->str, &tmp);
>  		if (tmp == NULL) goto err;
> -		if (size <= prop_len) goto ret;
> -
> -		char *val = _get_property_value(tmp, size, prop, prop_len);
> -		if (val == NULL) goto ret;
> -
> -		g_string_append_printf(str, "\"%u\":{", vmid);
> -		_g_str_append_kv_jsonescaped(str, prop, val);
> -		g_string_append_c(str, '}');
>  
> +		_print_found_properties(str, tmp, size, props, num_props, vmid, values, min, max, 1);
>  	} else {
>  		GHashTableIter iter;
>  		g_hash_table_iter_init (&iter, ht);
> @@ -943,21 +1007,16 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
>  			tmp = NULL;
>  			// use memdb_read_nolock because lock is handled here
>  			int size = memdb_read_nolock(memdb, path->str, &tmp);
> -			if (tmp == NULL || size <= prop_len) continue;
> +			if (tmp == NULL) continue;
>  
> -			char *val = _get_property_value(tmp, size, prop, prop_len);
> -			if (val == NULL) continue;
> -
> -			if (!first) g_string_append_printf(str, ",\n");
> -			else first = 0;
> -
> -			g_string_append_printf(str, "\"%u\":{", vminfo->vmid);
> -			_g_str_append_kv_jsonescaped(str, prop, val);
> -			g_string_append_c(str, '}');
> +			memset(values, 0, sizeof(char*)*num_props); // reset array
> +			first = _print_found_properties(str, tmp, size, props, num_props,
> +					vminfo->vmid, values, min, max, first);
>  		}
>  	}
>  ret:
>  	g_free(tmp);
> +	free(values);
>  	if (path != NULL) {
>  		g_string_free(path, TRUE);
>  	}
> @@ -973,6 +1032,12 @@ enoent:
>  	goto ret;
>  }
>  
> +int
> +cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
> +{
> +	return cfs_create_guest_conf_properties_msg(str, memdb, &prop, 1, vmid);
> +}
> +
>  void
>  record_memdb_change(const char *path)
>  {
> diff --git a/data/src/status.h b/data/src/status.h
> index bbf0948..041cb34 100644
> --- a/data/src/status.h
> +++ b/data/src/status.h
> @@ -163,4 +163,7 @@ cfs_create_memberlist_msg(
>  int
>  cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid);
>  
> +int
> +cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid);
> +
>  #endif /* _PVE_STATUS_H_ */
> -- 
> 2.30.2




^ permalink raw reply	[flat|nested] 27+ messages in thread

* Re: [pve-devel] [PATCH cluster v9 4/4] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 4/4] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
@ 2022-11-14 13:32   ` Wolfgang Bumiller
  0 siblings, 0 replies; 27+ messages in thread
From: Wolfgang Bumiller @ 2022-11-14 13:32 UTC (permalink / raw)
  To: Dominik Csapak; +Cc: pve-devel

some nits

On Mon, Nov 14, 2022 at 10:43:48AM +0100, Dominik Csapak wrote:
> by adding a 'user-tag-privileges' and 'admin-tags' option.
> The first sets the policy by which "normal" users (with
> 'VM.Config.Options' on the respective guest) can create/delete tags
> and the second is a list of tags only settable by 'admins'
> ('Sys.Modify' on '/')
> 
> also add a helper 'get_allowed_tags' that returns the allowed (existing)
> tags, the privileged tags, and if a user can enter 'freeform' tags.
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> changes from v8:
> * renamed properties
> * improved descriptions
> * renamed get_user_admin_tag to get_allowed_tags and consider the user
>   priviliges, also return if we allow 'freeform' tags
> * smaller bug fixes (e.g. list reference handling in join)
> 
>  data/PVE/DataCenterConfig.pm | 107 +++++++++++++++++++++++++++++++++++
>  1 file changed, 107 insertions(+)
> 
> diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
> index 532e5e5..b689942 100644
> --- a/data/PVE/DataCenterConfig.pm
> +++ b/data/PVE/DataCenterConfig.pm
> @@ -154,6 +154,32 @@ my $tag_style_format = {
>      },
>  };
>  
> +my $user_tag_privs_format = {
> +    'user-allow' => {
> +	optional => 1,
> +	type => 'string',
> +	enum => ['none', 'list', 'existing', 'free'],
> +	default => 'free',
> +	description => "Controls tag usage for users without `Sys.Modify` on `/` bey either "
> +	    ."allowing `none`, a `list`, already `existing` or anything (`free`).",
> +	verbose_description => "Controls which tags can be set or deleted on resources an user "
> +	    ."controls (such as guests). Users iwth the `Sys.Modify` privilege on `/` are always "
> +	    ." unrestricted. "
> +	    ."'none' means no tags are modifiable. "
> +	    ."'list' allows tags from the given list. "
> +	    ."'existing' means only already existing tags of resources able to access or from the"
> +	    ."given list. "
> +	    ."'free' means users can assign any tags."
> +    },
> +    'user-allow-list' => {
> +	optional => 1,
> +	type => 'string',
> +	pattern => "${PVE::JSONSchema::PVE_TAG_RE}(?:\;${PVE::JSONSchema::PVE_TAG_RE})*",
> +	typetext => "<tag>[;<tag>...]",
> +	description => "List of tags users are allowed to set and delete (semicolon separated).",
> +    },
> +};
> +
>  my $datacenter_schema = {
>      type => "object",
>      additionalProperties => 0,
> @@ -285,12 +311,68 @@ my $datacenter_schema = {
>  	    description => "Tag style options.",
>  	    format => $tag_style_format,
>  	},
> +	'user-tag-access' => {
> +	    optional => 1,
> +	    type => 'string',
> +	    description => "Privilege options for user settable tags",
> +	    format => $user_tag_privs_format,
> +	},
> +	'privileged-tags' => {
> +	    optional => 1,
> +	    type => 'string',
> +	    description => "A list of tags that require a `Sys.Modify` on '/') to set and delete. "
> +		."Tags set here that are also in 'user-tag-access' also require `Sys.Modify`.",
> +	    pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
> +	    typetext => "<tag>[;<tag>...]",
> +	},
>      },
>  };
>  
>  # make schema accessible from outside (for documentation)
>  sub get_datacenter_schema { return $datacenter_schema };
>  
> +# in scalar context, returns the list of allowed tags that exist
> +# in list context, returns a tuple of allowed tags, privileged tags, and if freeform is enabled
> +#
> +# requires an instance of rpcenv and a user to check the correct privileges for the allowed tags
> +sub get_allowed_tags {
> +    my ($rpcenv, $user) = @_;
> +
> +    my $privileged_user = $rpcenv->check($user, '/', ['Sys.Modify'], 1);
> +
> +    my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
> +
> +    my $freeform = 1;
> +    my $allowed_tags = {};
> +    my $privileged_tags = {};
> +    if (my $tags = $dc->{'privileged-tags'}) {
> +	$privileged_tags->{$_} = 1 for $tags->@*;
> +    }
> +    my $user_tag_privs = $dc->{'user-tag-access'} // {};
> +    my $user_allow = $user_tag_privs->{'user-allow'} // 'free';
> +    $freeform = $user_allow eq 'free';
> +
> +    if ($user_allow ne 'none' || $privileged_user) {
> +	map { $allowed_tags->{$_} = 1 } ($user_tag_privs->{'user-allow-list'} // [])->@*;

use `for` instead of map

> +    }
> +
> +    if ($user_allow eq 'free' || $user_allow eq 'existing' || $privileged_user) {
> +	my $props = PVE::Cluster::get_guest_config_properties(['tags']);
> +	for my $vmid (keys $props->%*) {
> +	    next if !$privileged_user && !$rpcenv->check_vm_perm($user, $vmid, undef, ['VM.Audit'], 1, 1);
> +	    map { $allowed_tags->{$_} = 1 } PVE::Tools::split_list($props->{$vmid}->{tags});

^ same

> +	}
> +    }
> +
> +    if ($privileged_user) {
> +	$allowed_tags->{$_} = 1 for keys $privileged_tags->%*;

^
( like you're doing here)
v

> +    } else {
> +	delete $allowed_tags->{$_} for keys $privileged_tags->%*;
> +    }
> +
> +    return wantarray ? ($allowed_tags, $privileged_tags, $freeform) : $allowed_tags;
> +}
> +
>  sub parse_datacenter_config {
>      my ($filename, $raw) = @_;
>  
> @@ -333,6 +415,19 @@ sub parse_datacenter_config {
>  	$res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
>      }
>  
> +    if (my $user_tag_privs = $res->{'user-tag-access'}) {
> +	$res->{'user-tag-access'} =
> +	    parse_property_string($user_tag_privs_format, $user_tag_privs);
> +
> +	    if (my $user_tags = $res->{'user-tag-access'}->{'user-allow-list'}) {

^
wrong indentation?
v

> +	        $res->{'user-tag-access'}->{'user-allow-list'} = [split(';', $user_tags)];
> +	    }
> +    }
> +
> +    if (my $admin_tags = $res->{'privileged-tags'}) {
> +	$res->{'privileged-tags'} = [split(';', $admin_tags)];
> +    }
> +
>      # for backwards compatibility only, new migration property has precedence
>      if (defined($res->{migration_unsecure})) {
>  	if (defined($res->{migration}->{type})) {
> @@ -396,6 +491,18 @@ sub write_datacenter_config {
>  	$cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
>      }
>  
> +    if (ref(my $user_tag_privs = $cfg->{'user-tag-access'})) {
> +	if (my $user_tags = $user_tag_privs->{'user-allow-list'}) {
> +	    $user_tag_privs->{'user-allow-list'} = join(';', sort $user_tags->@*);
> +	}
> +	$cfg->{'user-tag-access'} =
> +	    PVE::JSONSchema::print_property_string($user_tag_privs, $user_tag_privs_format);
> +    }
> +
> +    if (ref(my $admin_tags = $cfg->{'privileged-tags'})) {
> +	$cfg->{'privileged-tags'} = join(';', sort $admin_tags->@*);
> +    }
> +
>      my $comment = '';
>      # add description as comment to top of file
>      my $description = $cfg->{description} || '';
> -- 
> 2.30.2




^ permalink raw reply	[flat|nested] 27+ messages in thread

* Re: [pve-devel] [PATCH container v9 1/1] check_ct_modify_config_perm: improve tag privilege check
  2022-11-14  9:43 ` [pve-devel] [PATCH container v9 1/1] check_ct_modify_config_perm: " Dominik Csapak
@ 2022-11-14 13:37   ` Wolfgang Bumiller
  0 siblings, 0 replies; 27+ messages in thread
From: Wolfgang Bumiller @ 2022-11-14 13:37 UTC (permalink / raw)
  To: Dominik Csapak; +Cc: pve-devel

nits & dedup question

On Mon, Nov 14, 2022 at 10:43:50AM +0100, Dominik Csapak wrote:
> 'normal' tags require 'VM.Config.Options' on '/vm/<vmid>', but not
> allowed tags (either limited with 'user-tag-access' or 'privileged-tags'
> in the datacenter config) require 'Sys.Modify' on '/'
> 
> this patch implements the proper checks on adding/editing/deleting
> these permissions
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> changes from v8:
> * adapt to 'get_allowed_tags'
> * cache privilege checks (so we don't have to do it when many tags change)
>  src/PVE/LXC.pm | 42 ++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 42 insertions(+)
> 
> diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> index 4bbd739..44dee7e 100644
> --- a/src/PVE/LXC.pm
> +++ b/src/PVE/LXC.pm
> @@ -1336,6 +1336,48 @@ sub check_ct_modify_config_perm {
>  	} elsif ($opt eq 'hookscript') {
>  	    # For now this is restricted to root@pam
>  	    raise_perm_exc("changing the hookscript is only allowed for root\@pam");
> +	} elsif ($opt eq 'tags') {

Could we put this entire block into guest-common somehow?

> +	    my $allowed_tags;
> +	    my $privileged_tags;
> +	    my $freeform;
> +	    my $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1);
> +	    my $has_config_options =
> +		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'], 1);
> +
> +	    my $check_tag_perms = sub {

^ or at leas this?

As this code is duplicated in qemu-server

> +		my ($tag) = @_;
> +
> +		return if $privileged_user;
> +
> +		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'])
> +		    if !$has_config_options;
> +
> +		if (!defined($allowed_tags) && !defined($privileged_tags) && !defined($freeform)) {
> +		    ($allowed_tags, $privileged_tags, $freeform)
> +			= PVE::DataCenterConfig::get_allowed_tags($rpcenv, $authuser);
> +		}
> +
> +		if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
> +		    die "'Sys.Modify' required on '/' for modifying tag $tag\n" if !$privileged_user;
> +		}
> +	    };
> +
> +	    my $old_tags = {};
> +	    my $new_tags = {};
> +
> +	    map { $old_tags->{$_} += 1 } PVE::Tools::split_list($oldconf->{$opt} // '');
> +	    map { $new_tags->{$_} += 1 } PVE::Tools::split_list($newconf->{$opt});

^ map -> for

> +
> +	    my $check_tags = sub {
> +		my ($a, $b) = @_;
> +		foreach my $tag (keys %$a) {
> +		    next if ($b->{$tag} // 0) == ($a->{$tag} // 0);
> +		    $check_tag_perms->($tag);
> +		}
> +	    };
> +
> +	    $check_tags->($old_tags, $new_tags);
> +	    $check_tags->($new_tags, $old_tags);
>  	} else {
>  	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
>  	}
> -- 
> 2.30.2




^ permalink raw reply	[flat|nested] 27+ messages in thread

* Re: [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check
  2022-11-14  9:43 ` [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check Dominik Csapak
@ 2022-11-14 13:37   ` Wolfgang Bumiller
  2022-11-15  8:34   ` Aaron Lauterer
  1 sibling, 0 replies; 27+ messages in thread
From: Wolfgang Bumiller @ 2022-11-14 13:37 UTC (permalink / raw)
  To: Dominik Csapak; +Cc: pve-devel

some nits, but really, just read the container patch response instead
;-)

On Mon, Nov 14, 2022 at 10:43:49AM +0100, Dominik Csapak wrote:
> 'normal' tags require 'VM.Config.Options' on '/vm/<vmid>', but not
> allowed tags (either limited with 'user-tag-access' or 'privileged-tags'
> in the datacenter config) require 'Sys.Modify' on '/'
> 
> this patch implements the proper checks on adding/editing/deleting
> these permissions
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> changes from v8:
> * adapt to 'get_allowed_tags'
> * cache privilege checks (so we don't have to do it when many tags change)
>  PVE/API2/Qemu.pm | 53 +++++++++++++++++++++++++++++++++++++++++++++++-
>  1 file changed, 52 insertions(+), 1 deletion(-)
> 
> diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
> index 30348e6..269451c 100644
> --- a/PVE/API2/Qemu.pm
> +++ b/PVE/API2/Qemu.pm
> @@ -539,7 +539,6 @@ my $generaloptions = {
>      'startup' => 1,
>      'tdf' => 1,
>      'template' => 1,
> -    'tags' => 1,
>  };
>  
>  my $vmpoweroptions = {
> @@ -609,6 +608,7 @@ my $check_vm_modify_config_perm = sub {
>  	next if PVE::QemuServer::is_valid_drivename($opt);
>  	next if $opt eq 'cdrom';
>  	next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
> +	next if $opt eq 'tags';
>  
>  
>  	if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
> @@ -1631,6 +1631,31 @@ my $update_vm_api  = sub {
>  		}
>  	    };
>  
> +	    my $allowed_tags;
> +	    my $privileged_tags;
> +	    my $freeform;
> +	    my $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1);
> +	    my $has_config_options =
> +		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'], 1);
> +
> +	    my $check_tag_perms = sub {

^ See the container patch

> +		my ($tag) = @_;
> +
> +		return if $privileged_user;
> +
> +		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'])
> +		    if !$has_config_options;
> +
> +		if (!defined($allowed_tags) && !defined($privileged_tags) && !defined($freeform)) {
> +		    ($allowed_tags, $privileged_tags, $freeform)
> +			= PVE::DataCenterConfig::get_allowed_tags($rpcenv, $authuser);
> +		}
> +
> +		if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
> +		    die "'Sys.Modify' required on '/' for modifying tag $tag\n" if !$privileged_user;
> +		}
> +	    };
> +
>  	    foreach my $opt (@delete) {
>  		$modified->{$opt} = 1;
>  		$conf = PVE::QemuConfig->load_config($vmid); # update/reload
> @@ -1689,6 +1714,13 @@ my $update_vm_api  = sub {
>  		    }
>  		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
>  		    PVE::QemuConfig->write_config($vmid, $conf);
> +		} elsif ($opt eq 'tags') {
> +		    foreach my $tag (PVE::Tools::split_list($val)) {
> +			check_tag_perms->($tag);
> +		    }
> +
> +		    delete $conf->{$opt};
> +		    PVE::QemuConfig->write_config($vmid, $conf);
>  		} else {
>  		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
>  		    PVE::QemuConfig->write_config($vmid, $conf);
> @@ -1748,6 +1780,25 @@ my $update_vm_api  = sub {
>  		    } elsif ($authuser ne 'root@pam') {
>  			die "only root can modify '$opt' config for real devices\n";
>  		    }
> +		    $conf->{pending}->{$opt} = $param->{$opt};
> +		} elsif ($opt eq 'tags') {
> +		    my $old_tags = {};
> +		    my $new_tags = {};
> +
> +		    map { $old_tags->{$_} += 1 } PVE::Tools::split_list($conf->{$opt} // '');
> +		    map { $new_tags->{$_} += 1 } PVE::Tools::split_list($param->{$opt});

^ See the container patch

> +
> +		    my $check_tags = sub {
> +			my ($a, $b) = @_;
> +			foreach my $tag (keys %$a) {
> +			    next if ($b->{$tag} // 0) == ($a->{$tag} // 0);
> +			    $check_tag_perms->($tag);
> +			}
> +		    };
> +
> +		    $check_tags->($old_tags, $new_tags);
> +		    $check_tags->($new_tags, $old_tags);
> +
>  		    $conf->{pending}->{$opt} = $param->{$opt};
>  		} else {
>  		    $conf->{pending}->{$opt} = $param->{$opt};
> -- 
> 2.30.2




^ permalink raw reply	[flat|nested] 27+ messages in thread

* Re: [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui
  2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
                   ` (19 preceding siblings ...)
  2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 12/12] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
@ 2022-11-14 17:20 ` Aaron Lauterer
  20 siblings, 0 replies; 27+ messages in thread
From: Aaron Lauterer @ 2022-11-14 17:20 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

Gave it another test drive, and it works much better now.

The GUI to edit the priviledged tags and the predefined tags for the users work 
well.

Played around with a user with just VMAdmin permissions and the different 
options and how the drop down menu when adding a new tag is populated.


I think we have discussed this at some point in the past. I can add the same tag 
multiple times. Not sure if this is a wanted / accepted behavior.


With that, and some nits regarding the code tomorrow, consider this series

Tested-By: Aaron Lauterer <a.lauterer@proxmox.com>




^ permalink raw reply	[flat|nested] 27+ messages in thread

* Re: [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check
  2022-11-14  9:43 ` [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check Dominik Csapak
  2022-11-14 13:37   ` Wolfgang Bumiller
@ 2022-11-15  8:34   ` Aaron Lauterer
  1 sibling, 0 replies; 27+ messages in thread
From: Aaron Lauterer @ 2022-11-15  8:34 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

a small nit inline

On 11/14/22 10:43, Dominik Csapak wrote:
> 'normal' tags require 'VM.Config.Options' on '/vm/<vmid>', but not
> allowed tags (either limited with 'user-tag-access' or 'privileged-tags'
> in the datacenter config) require 'Sys.Modify' on '/'
> 
> this patch implements the proper checks on adding/editing/deleting
> these permissions
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> changes from v8:
> * adapt to 'get_allowed_tags'
> * cache privilege checks (so we don't have to do it when many tags change)
>   PVE/API2/Qemu.pm | 53 +++++++++++++++++++++++++++++++++++++++++++++++-
>   1 file changed, 52 insertions(+), 1 deletion(-)
> 
> diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
> index 30348e6..269451c 100644
> --- a/PVE/API2/Qemu.pm
> +++ b/PVE/API2/Qemu.pm
> @@ -539,7 +539,6 @@ my $generaloptions = {
>       'startup' => 1,
>       'tdf' => 1,
>       'template' => 1,
> -    'tags' => 1,
>   };
>   
>   my $vmpoweroptions = {
> @@ -609,6 +608,7 @@ my $check_vm_modify_config_perm = sub {
>   	next if PVE::QemuServer::is_valid_drivename($opt);
>   	next if $opt eq 'cdrom';
>   	next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
> +	next if $opt eq 'tags';
>   
>   
>   	if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
> @@ -1631,6 +1631,31 @@ my $update_vm_api  = sub {
>   		}
>   	    };
>   
> +	    my $allowed_tags;
> +	    my $privileged_tags;
> +	    my $freeform;
> +	    my $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1);
> +	    my $has_config_options =
> +		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'], 1);
> +
> +	    my $check_tag_perms = sub {
> +		my ($tag) = @_;
> +
> +		return if $privileged_user;
> +
> +		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'])
> +		    if !$has_config_options;
> +
> +		if (!defined($allowed_tags) && !defined($privileged_tags) && !defined($freeform)) {
> +		    ($allowed_tags, $privileged_tags, $freeform)
> +			= PVE::DataCenterConfig::get_allowed_tags($rpcenv, $authuser);
> +		}
> +
> +		if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
> +		    die "'Sys.Modify' required on '/' for modifying tag $tag\n" if !$privileged_user;
would be nice if the tag would be within '. Putting $tag into {} would be a bit 
cleaner as well:

... for modifying tag '${tag}'\n" ...


> +		}
> +	    };
[...]




^ permalink raw reply	[flat|nested] 27+ messages in thread

end of thread, other threads:[~2022-11-15  8:34 UTC | newest]

Thread overview: 27+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-11-14  9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
2022-11-14 13:15   ` Wolfgang Bumiller
2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 2/4] Cluster: add get_guest_config_properties Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH cluster v9 4/4] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
2022-11-14 13:32   ` Wolfgang Bumiller
2022-11-14  9:43 ` [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check Dominik Csapak
2022-11-14 13:37   ` Wolfgang Bumiller
2022-11-15  8:34   ` Aaron Lauterer
2022-11-14  9:43 ` [pve-devel] [PATCH container v9 1/1] check_ct_modify_config_perm: " Dominik Csapak
2022-11-14 13:37   ` Wolfgang Bumiller
2022-11-14  9:43 ` [pve-devel] [PATCH widget-toolkit v9 1/2] add tag related helpers Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH widget-toolkit v9 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 01/12] api: /cluster/resources: add tags to returned properties Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 02/12] api: add /ui-options api call Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 04/12] ui: parse and save tag infos from /ui-options Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 05/12] ui: add form/TagColorGrid Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 06/12] ui: add PVE.form.ListField Dominik Csapak
2022-11-14  9:43 ` [pve-devel] [PATCH manager v9 07/12] ui: dc/OptionView: add editors for tag settings Dominik Csapak
2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 08/12] ui: add form/Tag Dominik Csapak
2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 09/12] ui: add form/TagEdit.js Dominik Csapak
2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 11/12] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
2022-11-14  9:44 ` [pve-devel] [PATCH manager v9 12/12] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
2022-11-14 17:20 ` [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Aaron Lauterer

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal