public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui
@ 2022-11-15 13:02 Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
                   ` (22 more replies)
  0 siblings, 23 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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 overridden 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 v9:
* implemented changes required by wolfgang
  - fixed some c code behaviour
  - refactored the permission check into guest-common
* changed a bit how the permission check worked:
  - get_allowed_tags now takes a bool (privileged_user) and a closure
    for checking the vm access. with this we can avoid a
    cluster <-> access-control cyclic dependency
  - refactored some of the internal code of the permission check
    (see patch for details)
  - the forbidden tags are now in quotes in the error
* added an optional 'ordering' option into the 'tag-style' property
  with this the tags will be sorted in the gui (ui only)
  i left the patches separately

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

(omitted older changelog)

pve-cluster:

Dominik Csapak (5):
  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
  datacenter.cfg: add 'ordering' to 'tag-style' config

 data/PVE/Cluster.pm          |  27 +++++
 data/PVE/DataCenterConfig.pm | 149 ++++++++++++++++++++++++++++
 data/src/cfs-ipc-ops.h       |   2 +
 data/src/server.c            |  63 ++++++++++++
 data/src/status.c            | 184 ++++++++++++++++++++++++-----------
 data/src/status.h            |   3 +
 6 files changed, 370 insertions(+), 58 deletions(-)

pve-guest-common:

Dominik Csapak (1):
  GuestHelpers: add 'assert_tag_permissions'

 debian/control          |  3 ++-
 src/PVE/GuestHelpers.pm | 54 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 55 insertions(+), 2 deletions(-)

qemu-server:

Dominik Csapak (1):
  api: update: check for tags permissions with 'assert_tag_permissions'

 PVE/API2/Qemu.pm | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

pve-container:

Dominik Csapak (1):
  check_ct_modify_config_perm: check for tags permissions with
    'assert_tag_permissions'

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

proxmox-widget-toolkit:

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 | 52 ++++++++++++++++++++++++++
 3 files changed, 156 insertions(+)

pve-manager:

Dominik Csapak (13):
  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
  ui: implement tag ordering from datacenter.cfg

 PVE/API2.pm                            |  61 +++++
 PVE/API2/Cluster.pm                    |   9 +-
 www/css/ext6-pve.css                   |  57 ++++
 www/manager6/Makefile                  |   4 +
 www/manager6/Utils.js                  |  95 ++++++-
 www/manager6/Workspace.js              |   2 +
 www/manager6/data/ResourceStore.js     |   7 +
 www/manager6/dc/OptionView.js          | 221 ++++++++++++++-
 www/manager6/form/GlobalSearchField.js |  20 +-
 www/manager6/form/ListField.js         | 165 ++++++++++++
 www/manager6/form/Tag.js               | 232 ++++++++++++++++
 www/manager6/form/TagColorGrid.js      | 357 +++++++++++++++++++++++++
 www/manager6/form/TagEdit.js           | 336 +++++++++++++++++++++++
 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, 1630 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] 44+ messages in thread

* [pve-devel] [PATCH cluster v10 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-16  9:50   ` Wolfgang Bumiller
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 2/5] Cluster: add get_guest_config_properties Dominik Csapak
                   ` (21 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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>
---
changes from v9:
* line_len, prop_len -> size_t
* document min/max
* remove setting of null byte in loop (it's already split at the null byte)
* improved the loop for skipping whitespace at the beginning
 data/src/cfs-ipc-ops.h |   2 +
 data/src/server.c      |  63 ++++++++++++++
 data/src/status.c      | 184 ++++++++++++++++++++++++++++-------------
 data/src/status.h      |   3 +
 4 files changed, 194 insertions(+), 58 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..80f838b 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,62 @@ 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 || current[proplen] != '\0') {
+				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;
+			    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 validity 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..47cb42e 100644
--- a/data/src/status.c
+++ b/data/src/status.c
@@ -804,25 +804,55 @@ 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, size_t line_len, const char *prop, size_t 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];
+		char *v_end = &line[line_len - 1];
+
+		// drop initial value whitespaces here already
+		while (v_end > v_start && *v_start && isspace(*v_start)) v_start++;
+
+		if (!*v_start) return NULL;
+
+		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
+//
+// min..max is the char range of the first character of the given props,
+// so that we can return early when checking the line
+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 +860,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;
-
-		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;
+		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;
+
+		size_t line_len = strlen(line);
+		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 +904,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 +993,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 +1010,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 +1035,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] 44+ messages in thread

* [pve-devel] [PATCH cluster v10 2/5] Cluster: add get_guest_config_properties
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 3/5] datacenter.cfg: add option for tag-style Dominik Csapak
                   ` (20 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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] 44+ messages in thread

* [pve-devel] [PATCH cluster v10 3/5] datacenter.cfg: add option for tag-style
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 2/5] Cluster: add get_guest_config_properties Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
                   ` (19 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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>
---
 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] 44+ messages in thread

* [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (2 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 3/5] datacenter.cfg: add option for tag-style Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 15:17   ` Fabian Grünbichler
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config Dominik Csapak
                   ` (18 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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 v9:
* get_allowed_tags now takes a bool + closure for checking the tag
  access, prevents cyclic dependency between cluster and access-control
* use 'for' instead of 'map'
* fix indentation
 data/PVE/DataCenterConfig.pm | 105 +++++++++++++++++++++++++++++++++++
 1 file changed, 105 insertions(+)

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 532e5e5..f7f90e9 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,66 @@ 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
+#
+# first parameter is a bool if the user is 'privileged' (normally Sys.Modify on /)
+# second parameter is a closure which takes the vmid. should check if the user can see the vm tags
+sub get_allowed_tags {
+    my ($privileged_user, $can_see_vm_tags) = @_;
+
+    my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
+
+    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';
+    my $freeform = $user_allow eq 'free';
+
+    if ($user_allow ne 'none' || $privileged_user) {
+	$allowed_tags->{$_} = 1 for ($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 && !$can_see_vm_tags->($vmid);
+	    $allowed_tags->{$_} = 1 for 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 +413,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 +489,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] 44+ messages in thread

* [pve-devel] [PATCH cluster v10 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (3 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH guest-common v10 1/1] GuestHelpers: add 'assert_tag_permissions' Dominik Csapak
                   ` (17 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 UTC (permalink / raw)
  To: pve-devel

such that the admin can decide if the tags should be sorted in the gui

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

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index ae138f9..658cfd9 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -152,6 +152,13 @@ my $tag_style_format = {
 	typetext => '<tag>:<hex-color>[:<hex-color-for-text>][;<tag>=...]',
 	description => "Manual color mapping for tags (semicolon separated).",
     },
+    ordering => {
+	optional => 1,
+	type => 'string',
+	enum => ['config', 'alphabetical'],
+	default => 'config',
+	description => 'Controls the sorting of the tags in the web ui.',
+    }
 };
 
 my $user_tag_privs_format = {
-- 
2.30.2





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

* [pve-devel] [PATCH guest-common v10 1/1] GuestHelpers: add 'assert_tag_permissions'
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (4 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 15:34   ` Fabian Grünbichler
  2022-11-15 13:02 ` [pve-devel] [PATCH qemu-server v10 1/1] api: update: check for tags permissions with 'assert_tag_permissions' Dominik Csapak
                   ` (16 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 UTC (permalink / raw)
  To: pve-devel

helper to check permissions for tag setting/updating/deleting
for both container and qemu-server

gets the list of allowed tags from the DataCenterConfig and the current
user permissions, and checks for each tag that is added/removed if
the user has permissions to modify it

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

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
new in v10, but the code is mostly the same as the previous qemu-server
container one, with a few changes:
* adapt to new get_allowed_tags signature
* change 'map {foo} bar' into 'foo for bar'
* save all tags into $all_tags so that we only have to loop once
* use raise_perm_exc instead of die for permission errors (sets the
  correct http status code)
 debian/control          |  3 ++-
 src/PVE/GuestHelpers.pm | 54 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/debian/control b/debian/control
index 83bade3..ffff22b 100644
--- a/debian/control
+++ b/debian/control
@@ -12,7 +12,8 @@ Homepage: https://www.proxmox.com
 
 Package: libpve-guest-common-perl
 Architecture: all
-Depends: libpve-cluster-perl,
+Depends: libpve-access-control,
+         libpve-cluster-perl,
          libpve-common-perl (>= 7.2-6),
          libpve-storage-perl (>= 7.0-14),
          pve-cluster,
diff --git a/src/PVE/GuestHelpers.pm b/src/PVE/GuestHelpers.pm
index 0fe3fd6..2742501 100644
--- a/src/PVE/GuestHelpers.pm
+++ b/src/PVE/GuestHelpers.pm
@@ -3,6 +3,7 @@ package PVE::GuestHelpers;
 use strict;
 use warnings;
 
+use PVE::Exception qw(raise_perm_exc);
 use PVE::Tools;
 use PVE::Storage;
 
@@ -11,7 +12,7 @@ use Scalar::Util qw(weaken);
 
 use base qw(Exporter);
 
-our @EXPORT_OK = qw(safe_string_ne safe_boolean_ne safe_num_ne typesafe_ne);
+our @EXPORT_OK = qw(safe_string_ne safe_boolean_ne safe_num_ne typesafe_ne assert_tag_permissions);
 
 # We use a separate lock to block migration while a replication job
 # is running.
@@ -246,4 +247,55 @@ sub config_with_pending_array {
     return $res;
 }
 
+# checks the permissions for setting/updating/removing tags for guests
+# tagopt_old and tagopt_new expect the tags as they are in the config
+#
+# either returns gracefully or raises a permission exception
+sub assert_tag_permissions {
+    my ($vmid, $tagopt_old, $tagopt_new, $rpcenv, $authuser) = @_;
+
+    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'], 0, 1);
+
+    my $check_single_tag = 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(
+		$privileged_user,
+		sub {
+		    my ($vmid_to_check) = @_;
+		    return $rpcenv->check_vm_perm($authuser, $vmid_to_check, undef, ['VM.Audit'], 0, 1);
+		},
+	    );
+	}
+
+	if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
+	    raise_perm_exc("/, Sys.Modify for modifying tag '$tag'");
+	}
+
+	return;
+    };
+
+    my $old_tags = {};
+    my $new_tags = {};
+    my $all_tags = {};
+
+    $all_tags->{$_} = $old_tags->{$_} += 1 for PVE::Tools::split_list($tagopt_old // '');
+    $all_tags->{$_} = $new_tags->{$_} += 1 for PVE::Tools::split_list($tagopt_new // '');
+
+    for my $tag (keys $all_tags->%*) {
+	next if ($new_tags->{$tag} // 0) == ($old_tags->{$tag} // 0);
+	$check_single_tag->($tag);
+    }
+}
+
 1;
-- 
2.30.2





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

* [pve-devel] [PATCH qemu-server v10 1/1] api: update: check for tags permissions with 'assert_tag_permissions'
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (5 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH guest-common v10 1/1] GuestHelpers: add 'assert_tag_permissions' Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH container v10 1/1] check_ct_modify_config_perm: " Dominik Csapak
                   ` (15 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 UTC (permalink / raw)
  To: pve-devel

from GuestHelpers. This function checks all necessary permissions and
raises an exception if the user does not have the correct ones.

This is necessary for the new 'privileged' tags and 'user-tag-access'
permissions to work.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v9:
* use GuestHelpers::assert_tag_permissions
 PVE/API2/Qemu.pm | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index 30348e6..848a1bc 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -18,7 +18,7 @@ use PVE::Storage;
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::RESTHandler;
 use PVE::ReplicationConfig;
-use PVE::GuestHelpers;
+use PVE::GuestHelpers qw(assert_tag_permissions);
 use PVE::QemuConfig;
 use PVE::QemuServer;
 use PVE::QemuServer::Cloudinit;
@@ -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+$/) {
@@ -1689,6 +1689,10 @@ my $update_vm_api  = sub {
 		    }
 		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
 		    PVE::QemuConfig->write_config($vmid, $conf);
+		} elsif ($opt eq 'tags') {
+		    assert_tag_permissions($vmid, $val, '', $rpcenv, $authuser);
+		    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);
@@ -1749,6 +1753,9 @@ my $update_vm_api  = sub {
 			die "only root can modify '$opt' config for real devices\n";
 		    }
 		    $conf->{pending}->{$opt} = $param->{$opt};
+		} elsif ($opt eq 'tags') {
+		    assert_tag_permissions($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
+		    $conf->{pending}->{$opt} = $param->{$opt};
 		} else {
 		    $conf->{pending}->{$opt} = $param->{$opt};
 
-- 
2.30.2





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

* [pve-devel] [PATCH container v10 1/1] check_ct_modify_config_perm: check for tags permissions with 'assert_tag_permissions'
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (6 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH qemu-server v10 1/1] api: update: check for tags permissions with 'assert_tag_permissions' Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 1/2] add tag related helpers Dominik Csapak
                   ` (14 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 UTC (permalink / raw)
  To: pve-devel

from GuestHelpers. This function checks all necessary permissions and
raises an exception if the user does not have the correct ones.

This is necessary for the new 'privileged' tags and 'user-tag-access'
permissions to work.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v9:
* use GuestHelpers::assert_tag_permissions
 src/PVE/LXC.pm | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 4bbd739..3cedc9a 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -1336,6 +1336,10 @@ 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 $old = $oldconf->{$opt};
+	    my $new = $delete ? '' : $newconf->{$opt};
+	    PVE::GuestHelpers::assert_tag_permissions($vmid, $old, $new, $rpcenv, $authuser);
 	} else {
 	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
 	}
-- 
2.30.2





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

* [pve-devel] [PATCH widget-toolkit v10 1/2] add tag related helpers
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (7 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH container v10 1/1] check_ct_modify_config_perm: " Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-16 13:48   ` [pve-devel] applied: " Thomas Lamprecht
  2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
                   ` (13 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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>
---
changes from v9:
* tags are now 'inline-block' and for boundlists have a line-height
  of 15px (fixes an overflow issue)
 src/Utils.js         | 88 ++++++++++++++++++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 52 ++++++++++++++++++++++++++
 2 files changed, 140 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..37cadde 100644
--- a/src/css/ext6-pmx.css
+++ b/src/css/ext6-pmx.css
@@ -6,6 +6,58 @@
     background-color: LightYellow;
 }
 
+.proxmox-tags-full .proxmox-tag-light,
+.proxmox-tags-full .proxmox-tag-dark {
+    border-radius: 3px;
+    padding: 1px 6px;
+    margin: 0px 1px;
+    display: inline-block;
+}
+
+.x-boundlist-item > .proxmox-tag-light,
+.x-boundlist-item > .proxmox-tag-dark {
+    line-height: 15px;
+}
+
+
+.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] 44+ messages in thread

* [pve-devel] [PATCH widget-toolkit v10 2/2] Toolkit: add override for Ext.dd.DragDropManager
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (8 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 1/2] add tag related helpers Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-16 13:49   ` [pve-devel] applied: " Thomas Lamprecht
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 01/13] api: /cluster/resources: add tags to returned properties Dominik Csapak
                   ` (12 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 01/13] api: /cluster/resources: add tags to returned properties
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (9 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-16  8:02   ` Thomas Lamprecht
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 02/13] api: add /ui-options api call Dominik Csapak
                   ` (11 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 02/13] api: add /ui-options api call
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (10 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 01/13] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 03/13] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
                   ` (10 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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>
---
 PVE/API2.pm | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)

diff --git a/PVE/API2.pm b/PVE/API2.pm
index a42561604..7c1d0830d 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,64 @@ __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 $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1);
+
+	my $tags = PVE::DataCenterConfig::get_allowed_tags($privileged_user, sub {
+	    my ($vmid) = @_;
+	    return $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Audit'], 0, 1);
+	});
+
+	$res->{'allowed-tags'} = [sort keys $tags->%*];
+
+	return $res;
+    }});
+
 1;
-- 
2.30.2





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

* [pve-devel] [PATCH manager v10 03/13] ui: call '/ui-options' and save the result in PVE.UIOptions
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (11 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 02/13] api: add /ui-options api call Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 04/13] ui: parse and save tag infos from /ui-options Dominik Csapak
                   ` (9 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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>
---
 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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 04/13] ui: parse and save tag infos from /ui-options
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (12 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 03/13] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 05/13] ui: add form/TagColorGrid Dominik Csapak
                   ` (8 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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 v9:
* remove unused global 'override' event
* refactored the UIOptions update function and updateTagSettings a bit
  (now takes the full 'tag-style' object and handles it)
 www/manager6/Utils.js | 56 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 55 insertions(+), 1 deletion(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 29bf667b4..4ff8a1a55 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1860,10 +1860,64 @@ Ext.define('PVE.Utils', {
 	    url: '/ui-options',
 	    method: 'GET',
 	    success: function(response) {
-		PVE.UIOptions = response?.result?.data ?? {};
+		PVE.UIOptions = response?.result?.data ?? {
+		    'allowed-tags': [],
+		};
+
+		PVE.Utils.updateTagList(PVE.UIOptions['allowed-tags']);
+		PVE.Utils.updateTagSettings(PVE.UIOptions?.['tag-style']);
 	    },
 	});
     },
+
+    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);
+    },
+
+    updateTagSettings: function(style) {
+	let overrides = style?.['color-map'];
+	PVE.Utils.updateTagOverrides(PVE.Utils.parseTagOverrides(overrides ?? ""));
+
+	let shape = style?.shape ?? 'circle';
+	if (shape === '__default__') {
+	    style = 'circle';
+	}
+
+	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${shape}`);
+	PVE.data.ResourceStore.fireEvent('load');
+	Ext.GlobalEvents.fireEvent('loadedUiOptions');
+    },
 },
 
     singleton: true,
-- 
2.30.2





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

* [pve-devel] [PATCH manager v10 05/13] ui: add form/TagColorGrid
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (13 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 04/13] ui: parse and save tag infos from /ui-options Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 06/13] ui: add PVE.form.ListField Dominik Csapak
                   ` (7 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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 2802cbacd..572aa9392 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 4ff8a1a55..7cb77083d 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1918,6 +1918,8 @@ Ext.define('PVE.Utils', {
 	PVE.data.ResourceStore.fireEvent('load');
 	Ext.GlobalEvents.fireEvent('loadedUiOptions');
     },
+
+    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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 06/13] ui: add PVE.form.ListField
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (14 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 05/13] ui: add form/TagColorGrid Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 07/13] ui: dc/OptionView: add editors for tag settings Dominik Csapak
                   ` (6 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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>
---
 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 572aa9392..83c2effcf 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] 44+ messages in thread

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

namely for 'tag-tree-style' and 'tag-colors'.
display the tag overrides directly as they will appear as tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v9:
* now also sets PVE.UIOptions
* adapt to change of updateTagSettings
 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 7cb77083d..2e819ce84 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1919,6 +1919,26 @@ Ext.define('PVE.Utils', {
 	Ext.GlobalEvents.fireEvent('loadedUiOptions');
     },
 
+    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..ef2989d6c 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;
 	    }
+
+	    PVE.UIOptions['tag-style'] = store.getById('tag-style')?.data?.value;
+	    PVE.Utils.updateTagSettings(PVE.UIOptions['tag-style']);
 	});
 
 	me.on('activate', me.rstore.startUpdate);
-- 
2.30.2





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

* [pve-devel] [PATCH manager v10 08/13] ui: add form/Tag
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (16 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 07/13] ui: dc/OptionView: add editors for tag settings Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-16 14:57   ` Thomas Lamprecht
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js Dominik Csapak
                   ` (4 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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 v9:
* don't listen to the global event anymore
 www/css/ext6-pve.css     |  32 ++++++
 www/manager6/Makefile    |   1 +
 www/manager6/form/Tag.js | 232 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 265 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 83c2effcf..21be8da8e 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..cc4413d29
--- /dev/null
+++ b/www/manager6/form/Tag.js
@@ -0,0 +1,232 @@
+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();
+    },
+
+    destroy: function() {
+	let me = this;
+	if (me.picker) {
+	    Ext.destroy(me.picker);
+	}
+	me.callParent();
+    },
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (17 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 08/13] ui: add form/Tag Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-16 15:00   ` Thomas Lamprecht
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
                   ` (3 subsequent siblings)
  22 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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 v9:
* listen to glable event and render list of tags new
 www/css/ext6-pve.css         |  23 ++-
 www/manager6/Makefile        |   1 +
 www/manager6/form/TagEdit.js | 325 +++++++++++++++++++++++++++++++++++
 3 files changed, 345 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 21be8da8e..481c2bebc 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..7200e3c09
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,325 @@
+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);
+	    }
+
+	    me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
+		me.loadTags(me.oldTags, true); // refresh tag colors
+	    });
+	},
+    },
+
+    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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (18 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 11/13] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
                   ` (2 subsequent siblings)
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 11/13] ui: tree/ResourceTree: show Tags in tree
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (19 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 12/13] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 13/13] ui: implement tag ordering from datacenter.cfg Dominik Csapak
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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>
---
 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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 12/13] ui: add tags to ResourceGrid and GlobalSearchField
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (20 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 11/13] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 13/13] ui: implement tag ordering from datacenter.cfg Dominik Csapak
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 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] 44+ messages in thread

* [pve-devel] [PATCH manager v10 13/13] ui: implement tag ordering from datacenter.cfg
  2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
                   ` (21 preceding siblings ...)
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 12/13] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
@ 2022-11-15 13:02 ` Dominik Csapak
  22 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-15 13:02 UTC (permalink / raw)
  To: pve-devel

ui-options/datacenter.cfg return an 'ordering' option. parse that and
use it to order the tags when viewing.

when having that enabled, drag & drop when editing is disabled and the
tags will be inserted at the right place. when saving, the sorted
order will be written into the config

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
new in v10
 www/css/ext6-pve.css          |  5 +++++
 www/manager6/Utils.js         |  7 +++++++
 www/manager6/dc/OptionView.js | 17 +++++++++++++++++
 www/manager6/form/TagEdit.js  | 13 ++++++++++++-
 4 files changed, 41 insertions(+), 1 deletion(-)

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index 4fc83a878..b8c713c48 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -668,6 +668,11 @@ table.osds td:first-of-type {
     cursor: grab;
 }
 
+.hide-handles .pve-edit-tag > i.handle {
+    display: none;
+    padding-right: 0px;
+}
+
 .pve-edit-tag > i.action,
 .pve-add-tag > i.action {
     padding-left: 5px;
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 2e819ce84..d34ac53ac 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1931,6 +1931,9 @@ Ext.define('PVE.Utils', {
 	let text = '';
 	if (tagstext) {
 	    let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
+	    if (PVE.Utils.shouldSortTags()) {
+		tags = tags.sort();
+	    }
 	    text += ' ';
 	    tags.forEach((tag) => {
 		text += Proxmox.Utils.getTagElement(tag, overrides);
@@ -1939,6 +1942,10 @@ Ext.define('PVE.Utils', {
 	return text;
     },
 
+    shouldSortTags: function() {
+	return PVE.UIOptions?.['tag-style']?.ordering === 'alphabetical';
+    },
+
     tagCharRegex: /^[a-z0-9+_.-]$/i,
 },
 
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index ef2989d6c..65d057036 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -323,6 +323,7 @@ Ext.define('PVE.dc.OptionView', {
 		let shape = value.shape;
 		let shapeText = PVE.Utils.tagTreeStyles[shape] ?? Proxmox.Utils.defaultText;
 		let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText);
+		txt += `, ${Ext.String.format(gettext("Ordering: {0}"), value.ordering ?? 'config')}`;
 		if (Object.keys(colors).length > 0) {
 		    txt += ', ';
 		}
@@ -361,6 +362,9 @@ Ext.define('PVE.dc.OptionView', {
 			    if (values.shape) {
 				style.shape = values.shape;
 			    }
+			    if (values.ordering) {
+				style.ordering = values.ordering;
+			    }
 			    let value = PVE.Parser.printPropertyString(style);
 			    if (value === '') {
 				return {
@@ -380,6 +384,19 @@ Ext.define('PVE.dc.OptionView', {
 				defaultValue: '__default__',
 				deleteEmpty: true,
 			    },
+			    {
+				name: 'ordering',
+				xtype: 'proxmoxKVComboBox',
+				fieldLabel: gettext('Ordering'),
+				comboItems: [
+				    ['__default__', `${Proxmox.Utils.defaultText} (config)`],
+				    ['config', 'config'],
+				    ['alphabetical', 'alphabetical'],
+				],
+				defaultValue: '__default__',
+				value: '__default__',
+				deleteEmpty: true,
+			    },
 			    {
 				xtype: 'displayfield',
 				fieldLabel: gettext('Color Overrides'),
diff --git a/www/manager6/form/TagEdit.js b/www/manager6/form/TagEdit.js
index 7200e3c09..7219845ae 100644
--- a/www/manager6/form/TagEdit.js
+++ b/www/manager6/form/TagEdit.js
@@ -37,6 +37,8 @@ Ext.define('PVE.panel.TagEditContainer', {
 	onRender: function(v) {
 	    let me = this;
 	    let view = me.getView();
+	    view.toggleCls('hide-handles', PVE.Utils.shouldSortTags());
+
 	    view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
 		getDragData: function(e) {
 		    let source = e.getTarget('.handle');
@@ -168,6 +170,14 @@ Ext.define('PVE.panel.TagEditContainer', {
 	    let view = me.getView();
 	    let vm = me.getViewModel();
 	    let index = view.items.indexOf(me.lookup('addTagBtn'));
+	    if (PVE.Utils.shouldSortTags()) {
+		index = view.items.findIndexBy(tagField => {
+		    if (tagField.reference === 'addTagBtn') {
+			return true;
+		    }
+		    return tagField.tag >= tag;
+		}, 1);
+	    }
 	    view.insert(index, {
 		xtype: 'pmxTag',
 		tag,
@@ -226,7 +236,8 @@ Ext.define('PVE.panel.TagEditContainer', {
 	    }
 
 	    me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
-		me.loadTags(me.oldTags, true); // refresh tag colors
+		view.toggleCls('hide-handles', PVE.Utils.shouldSortTags());
+		me.loadTags(me.oldTags, true); // refresh tag colors and order
 	    });
 	},
     },
-- 
2.30.2





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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
@ 2022-11-15 15:17   ` Fabian Grünbichler
  2022-11-16  7:48     ` Thomas Lamprecht
  2022-11-16  8:47     ` Dominik Csapak
  0 siblings, 2 replies; 44+ messages in thread
From: Fabian Grünbichler @ 2022-11-15 15:17 UTC (permalink / raw)
  To: Proxmox VE development discussion

On November 15, 2022 2:02 pm, 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 v9:
> * get_allowed_tags now takes a bool + closure for checking the tag
>   access, prevents cyclic dependency between cluster and access-control
> * use 'for' instead of 'map'
> * fix indentation
>  data/PVE/DataCenterConfig.pm | 105 +++++++++++++++++++++++++++++++++++
>  1 file changed, 105 insertions(+)
> 
> diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
> index 532e5e5..f7f90e9 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 "

by

> +	    ."allowing `none`, a `list`, already `existing` or anything (`free`).",
> +	verbose_description => "Controls which tags can be set or deleted on resources an user "

"a" user

> +	    ."controls (such as guests). Users iwth the `Sys.Modify` privilege on `/` are always "

with

> +	    ." 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. "

this is hard to parse, maybe reword?

> +	    ."'free' means users can assign any tags."

this one is worded differently than the rest (maybe):

'none' no tags are modifiable.
'list' tags from 'user-allow-list' are modifiable.
'existing' like list, but already existing tags of resource are also modifiable.
'free' no tag restrictions.

> +    },
> +    '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).",

maybe add "for 'user-allow' values 'list' or 'existing'"?

> +    },
> +};
> +
>  my $datacenter_schema = {
>      type => "object",
>      additionalProperties => 0,
> @@ -285,12 +311,66 @@ 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",

user-settable

> +	    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>...]",

stray 'a' and ')' in first sentence.

I am not sure the second sentence is necessary, or rather, wouldn't it be better
to make the two lists mutually exclusive? e.g., by removing privileged tags from
the other list?

> +	},
>      },
>  };
>  
>  # 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
> +#

meh, this is a bit ugly, but okay

> +# first parameter is a bool if the user is 'privileged' (normally Sys.Modify on /)
> +# second parameter is a closure which takes the vmid. should check if the user can see the vm tags
> +sub get_allowed_tags {
> +    my ($privileged_user, $can_see_vm_tags) = @_;

couldn't this live somewhere else instead of having this weird interface? e.g.,
guest-common? it's not needed at all for parsing or writing the config, which is
what lives here in this module/repo/package..

the other priv related helper for this already lives there as well..

> +
> +    my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
> +
> +    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';
> +    my $freeform = $user_allow eq 'free';
> +
> +    if ($user_allow ne 'none' || $privileged_user) {
> +	$allowed_tags->{$_} = 1 for ($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 && !$can_see_vm_tags->($vmid);
> +	    $allowed_tags->{$_} = 1 for 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 +413,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)];

could (warn and?) filter out privileged tags here)

> +	}
> +    }
> +
> +    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 +489,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->@*);

same here

> +	}
> +	$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
> 
> 
> 
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
> 
> 
> 




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

* Re: [pve-devel] [PATCH guest-common v10 1/1] GuestHelpers: add 'assert_tag_permissions'
  2022-11-15 13:02 ` [pve-devel] [PATCH guest-common v10 1/1] GuestHelpers: add 'assert_tag_permissions' Dominik Csapak
@ 2022-11-15 15:34   ` Fabian Grünbichler
  0 siblings, 0 replies; 44+ messages in thread
From: Fabian Grünbichler @ 2022-11-15 15:34 UTC (permalink / raw)
  To: Proxmox VE development discussion

On November 15, 2022 2:02 pm, Dominik Csapak wrote:
> helper to check permissions for tag setting/updating/deleting
> for both container and qemu-server
> 
> gets the list of allowed tags from the DataCenterConfig and the current
> user permissions, and checks for each tag that is added/removed if
> the user has permissions to modify it
> 
> 'normal' tags require 'VM.Config.Options' on '/vms/<vmid>', but not
> allowed tags (either limited with 'user-tag-access' or
> 'privileged-tags' in the datacenter.cfg) requrie 'Sys.Modify' on '/'
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> new in v10, but the code is mostly the same as the previous qemu-server
> container one, with a few changes:
> * adapt to new get_allowed_tags signature
> * change 'map {foo} bar' into 'foo for bar'
> * save all tags into $all_tags so that we only have to loop once
> * use raise_perm_exc instead of die for permission errors (sets the
>   correct http status code)
>  debian/control          |  3 ++-
>  src/PVE/GuestHelpers.pm | 54 ++++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 55 insertions(+), 2 deletions(-)
> 
> diff --git a/debian/control b/debian/control
> index 83bade3..ffff22b 100644
> --- a/debian/control
> +++ b/debian/control
> @@ -12,7 +12,8 @@ Homepage: https://www.proxmox.com
>  
>  Package: libpve-guest-common-perl
>  Architecture: all
> -Depends: libpve-cluster-perl,
> +Depends: libpve-access-control,
> +         libpve-cluster-perl,
>           libpve-common-perl (>= 7.2-6),
>           libpve-storage-perl (>= 7.0-14),
>           pve-cluster,
> diff --git a/src/PVE/GuestHelpers.pm b/src/PVE/GuestHelpers.pm
> index 0fe3fd6..2742501 100644
> --- a/src/PVE/GuestHelpers.pm
> +++ b/src/PVE/GuestHelpers.pm
> @@ -3,6 +3,7 @@ package PVE::GuestHelpers;
>  use strict;
>  use warnings;
>  
> +use PVE::Exception qw(raise_perm_exc);
>  use PVE::Tools;
>  use PVE::Storage;
>  
> @@ -11,7 +12,7 @@ use Scalar::Util qw(weaken);
>  
>  use base qw(Exporter);
>  
> -our @EXPORT_OK = qw(safe_string_ne safe_boolean_ne safe_num_ne typesafe_ne);
> +our @EXPORT_OK = qw(safe_string_ne safe_boolean_ne safe_num_ne typesafe_ne assert_tag_permissions);
>  
>  # We use a separate lock to block migration while a replication job
>  # is running.
> @@ -246,4 +247,55 @@ sub config_with_pending_array {
>      return $res;
>  }
>  
> +# checks the permissions for setting/updating/removing tags for guests
> +# tagopt_old and tagopt_new expect the tags as they are in the config
> +#
> +# either returns gracefully or raises a permission exception
> +sub assert_tag_permissions {
> +    my ($vmid, $tagopt_old, $tagopt_new, $rpcenv, $authuser) = @_;
> +
> +    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'], 0, 1);
> +
> +    my $check_single_tag = sub {
> +	my ($tag) = @_;
> +	return if $privileged_user;
> +
> +	$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options'])
> +	    if !$has_config_options;
> +

nit: this confused me - but it's basically to only check once and fail if a tag has
changed? would be more readable by just recording whether it was checked already
and skipping based on that bool IMHO, instead of skipping based on the result of
a noerr redundant check..

> +	if (!defined($allowed_tags) && !defined($privileged_tags) && !defined($freeform)) {
> +	    ($allowed_tags, $privileged_tags, $freeform) = PVE::DataCenterConfig::get_allowed_tags(
> +		$privileged_user,
> +		sub {
> +		    my ($vmid_to_check) = @_;
> +		    return $rpcenv->check_vm_perm($authuser, $vmid_to_check, undef, ['VM.Audit'], 0, 1);
> +		},
> +	    );

see response to "get_allowed_tags" patch ;)

> +	}
> +
> +	if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
> +	    raise_perm_exc("/, Sys.Modify for modifying tag '$tag'");
> +	}
> +
> +	return;
> +    };
> +
> +    my $old_tags = {};
> +    my $new_tags = {};
> +    my $all_tags = {};
> +
> +    $all_tags->{$_} = $old_tags->{$_} += 1 for PVE::Tools::split_list($tagopt_old // '');
> +    $all_tags->{$_} = $new_tags->{$_} += 1 for PVE::Tools::split_list($tagopt_new // '');
> +
> +    for my $tag (keys $all_tags->%*) {
> +	next if ($new_tags->{$tag} // 0) == ($old_tags->{$tag} // 0);
> +	$check_single_tag->($tag);
> +    }
> +}
> +
>  1;
> -- 
> 2.30.2
> 
> 
> 
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
> 
> 
> 




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-15 15:17   ` Fabian Grünbichler
@ 2022-11-16  7:48     ` Thomas Lamprecht
  2022-11-16  8:47     ` Dominik Csapak
  1 sibling, 0 replies; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16  7:48 UTC (permalink / raw)
  To: Proxmox VE development discussion, Fabian Grünbichler

Am 15/11/2022 um 16:17 schrieb Fabian Grünbichler:
> this one is worded differently than the rest (maybe):
> 
> 'none' no tags are modifiable.
> 'list' tags from 'user-allow-list' are modifiable.
> 'existing' like list, but already existing tags of resource are also modifiable.
> 'free' no tag restrictions.

also switch "modifiable" to "useable" (or settable, if that's a word)? As
modifying here implies the tags as object themselves, which could be confusing
if we really mean that those are the ones a user can set or remove to a virtual
guest.






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

* Re: [pve-devel] [PATCH manager v10 01/13] api: /cluster/resources: add tags to returned properties
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 01/13] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-11-16  8:02   ` Thomas Lamprecht
  0 siblings, 0 replies; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16  8:02 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

would adapt this a bit, see inline, but no hard feelings

Am 15/11/2022 um 14:02 schrieb Dominik Csapak:
> 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);

props is a bit generic and having the prop_list here and below adds not much
value IMO, maybe rather here:

my $extra_guest_info = PVE::Cluster::get_guest_config_properties(['lock', 'tags']);

>  
>  	    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) {

and then here:

for my $key (keys $extra_guest_info->{$vmid}->%) {
    my $value = $extra_guest_info->{$vmid}->{$key} // next;
    $entry->{$prop} = $extra_guest_info->{$vmid}->{$key};
}

> +		    if (defined(my $value = $props->{$vmid}->{$prop})) {
> +			$entry->{$prop} = $value;
> +		    }
>  		}
>  
>  		if (defined($entry->{pool}) &&





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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-15 15:17   ` Fabian Grünbichler
  2022-11-16  7:48     ` Thomas Lamprecht
@ 2022-11-16  8:47     ` Dominik Csapak
  2022-11-16  8:51       ` Fabian Grünbichler
  2022-11-16  8:54       ` Thomas Lamprecht
  1 sibling, 2 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-16  8:47 UTC (permalink / raw)
  To: Proxmox VE development discussion, Fabian Grünbichler

most of the points are clear and ok for me, but
[snip]
>> +	    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>...]",
> 
> stray 'a' and ')' in first sentence.
> 
> I am not sure the second sentence is necessary, or rather, wouldn't it be better
> to make the two lists mutually exclusive? e.g., by removing privileged tags from
> the other list?

i don't really want to auto remove stuff from one option when set on another.
maybe it'd make more sense if we don't allow setting and admin tag when
it's already set in the 'user-allow-list' and vice versa? then
there cannot be a situation where a tag is in both lists at the same time?






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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  8:47     ` Dominik Csapak
@ 2022-11-16  8:51       ` Fabian Grünbichler
  2022-11-16  8:54       ` Thomas Lamprecht
  1 sibling, 0 replies; 44+ messages in thread
From: Fabian Grünbichler @ 2022-11-16  8:51 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox VE development discussion

On November 16, 2022 9:47 am, Dominik Csapak wrote:
> most of the points are clear and ok for me, but
> [snip]
>>> +	    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>...]",
>> 
>> stray 'a' and ')' in first sentence.
>> 
>> I am not sure the second sentence is necessary, or rather, wouldn't it be better
>> to make the two lists mutually exclusive? e.g., by removing privileged tags from
>> the other list?
> 
> i don't really want to auto remove stuff from one option when set on another.
> maybe it'd make more sense if we don't allow setting and admin tag when
> it's already set in the 'user-allow-list' and vice versa? then
> there cannot be a situation where a tag is in both lists at the same time?

forbidding it on the API level (and maybe, to catch bugs, when writing the
config) is only part of it though - such duplicates would need to be filtered
out when parsing as well, else they can sneak in via a manual config file edit.

but yeah, that would work as well I think.




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  8:47     ` Dominik Csapak
  2022-11-16  8:51       ` Fabian Grünbichler
@ 2022-11-16  8:54       ` Thomas Lamprecht
  2022-11-16  9:04         ` Dominik Csapak
  1 sibling, 1 reply; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16  8:54 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak,
	Fabian Grünbichler

Am 16/11/2022 um 09:47 schrieb Dominik Csapak:
>> I am not sure the second sentence is necessary, or rather, wouldn't it be better
>> to make the two lists mutually exclusive? e.g., by removing privileged tags from
>> the other list?
> 
> i don't really want to auto remove stuff from one option when set on another.
> maybe it'd make more sense if we don't allow setting and admin tag when
> it's already set in the 'user-allow-list' and vice versa? then
> there cannot be a situation where a tag is in both lists at the same time?
> 


Limits use cases, as we'll only ever allow priv'd tags to be used for things
like backup job guest-source selection, and there may be scenarios where an
admin wants to allow the user to set a specific privileged tags in the VMs
they control.

To make that work we'd:
- explicitly allow such listed tags for "normal" VM users even if they're in the
  privileged-tags (that's why I used the term "registered" in previous comments,
  might be better suited if we deem that privileged is then confusing)

- highlight the fact if a tag is in both


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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  8:54       ` Thomas Lamprecht
@ 2022-11-16  9:04         ` Dominik Csapak
  2022-11-16  9:10           ` Thomas Lamprecht
  0 siblings, 1 reply; 44+ messages in thread
From: Dominik Csapak @ 2022-11-16  9:04 UTC (permalink / raw)
  To: Thomas Lamprecht, Proxmox VE development discussion,
	Fabian Grünbichler

On 11/16/22 09:54, Thomas Lamprecht wrote:
> Am 16/11/2022 um 09:47 schrieb Dominik Csapak:
>>> I am not sure the second sentence is necessary, or rather, wouldn't it be better
>>> to make the two lists mutually exclusive? e.g., by removing privileged tags from
>>> the other list?
>>
>> i don't really want to auto remove stuff from one option when set on another.
>> maybe it'd make more sense if we don't allow setting and admin tag when
>> it's already set in the 'user-allow-list' and vice versa? then
>> there cannot be a situation where a tag is in both lists at the same time?
>>
> 
> 
> Limits use cases, as we'll only ever allow priv'd tags to be used for things
> like backup job guest-source selection, and there may be scenarios where an
> admin wants to allow the user to set a specific privileged tags in the VMs
> they control.
> 
> To make that work we'd:
> - explicitly allow such listed tags for "normal" VM users even if they're in the
>    privileged-tags (that's why I used the term "registered" in previous comments,
>    might be better suited if we deem that privileged is then confusing)
> 
> - highlight the fact if a tag is in both
> 

ok, then i have to change the permission checking code (currently i forbid
'normal' users the tag if it was in the 'privileged-tags' section, regardless
  if it was in the 'user-allow-list' or not)

how would you highlight that? a warning on the cli/syslog/etc. is not
visible, but on the ui we don't really have an obvious place to do so

i could try to add a seperate 'warning' row in the object grid when
that happens, not sure if that's what you meant though




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  9:04         ` Dominik Csapak
@ 2022-11-16  9:10           ` Thomas Lamprecht
  2022-11-16  9:31             ` Fabian Grünbichler
  0 siblings, 1 reply; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16  9:10 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak,
	Fabian Grünbichler

Am 16/11/2022 um 10:04 schrieb Dominik Csapak:
> On 11/16/22 09:54, Thomas Lamprecht wrote:
>> Am 16/11/2022 um 09:47 schrieb Dominik Csapak:
>>>> I am not sure the second sentence is necessary, or rather, wouldn't it be better
>>>> to make the two lists mutually exclusive? e.g., by removing privileged tags from
>>>> the other list?
>>>
>>> i don't really want to auto remove stuff from one option when set on another.
>>> maybe it'd make more sense if we don't allow setting and admin tag when
>>> it's already set in the 'user-allow-list' and vice versa? then
>>> there cannot be a situation where a tag is in both lists at the same time?
>>>
>>
>>
>> Limits use cases, as we'll only ever allow priv'd tags to be used for things
>> like backup job guest-source selection, and there may be scenarios where an
>> admin wants to allow the user to set a specific privileged tags in the VMs
>> they control.
>>
>> To make that work we'd:
>> - explicitly allow such listed tags for "normal" VM users even if they're in the
>>    privileged-tags (that's why I used the term "registered" in previous comments,
>>    might be better suited if we deem that privileged is then confusing)
>>
>> - highlight the fact if a tag is in both
>>
> 
> ok, then i have to change the permission checking code (currently i forbid
> 'normal' users the tag if it was in the 'privileged-tags' section, regardless
>  if it was in the 'user-allow-list' or not)

maybe wait on Fabian's opinion on that, I don't want to push this to strongly
but can imagine that it might be sensible and useful, and hard to change later.

> 
> how would you highlight that? a warning on the cli/syslog/etc. is not
> visible, but on the ui we don't really have an obvious place to do so
> 
> i could try to add a seperate 'warning' row in the object grid when
> that happens, not sure if that's what you meant though
> 

Syslog is never the place for such things, needs to happen on edit, and for
now there's no CLI so GUI is the only place we need to care about (edit cfgs
manually -> be on your own).

So a bottom section that shows a hints about the tags that are in both lists,
the hint would then be located in the edit windows for registered and allowed-list
of tags, so it doesn't necessarily needs to be inline (i.e., some highlight in
the existing tag edit).




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  9:10           ` Thomas Lamprecht
@ 2022-11-16  9:31             ` Fabian Grünbichler
  2022-11-16  9:38               ` Dominik Csapak
  2022-11-16  9:40               ` Thomas Lamprecht
  0 siblings, 2 replies; 44+ messages in thread
From: Fabian Grünbichler @ 2022-11-16  9:31 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox VE development discussion, Thomas Lamprecht

On November 16, 2022 10:10 am, Thomas Lamprecht wrote:
> Am 16/11/2022 um 10:04 schrieb Dominik Csapak:
>> On 11/16/22 09:54, Thomas Lamprecht wrote:
>>> Am 16/11/2022 um 09:47 schrieb Dominik Csapak:
>>>>> I am not sure the second sentence is necessary, or rather, wouldn't it be better
>>>>> to make the two lists mutually exclusive? e.g., by removing privileged tags from
>>>>> the other list?
>>>>
>>>> i don't really want to auto remove stuff from one option when set on another.
>>>> maybe it'd make more sense if we don't allow setting and admin tag when
>>>> it's already set in the 'user-allow-list' and vice versa? then
>>>> there cannot be a situation where a tag is in both lists at the same time?
>>>>
>>>
>>>
>>> Limits use cases, as we'll only ever allow priv'd tags to be used for things
>>> like backup job guest-source selection, and there may be scenarios where an
>>> admin wants to allow the user to set a specific privileged tags in the VMs
>>> they control.
>>>
>>> To make that work we'd:
>>> - explicitly allow such listed tags for "normal" VM users even if they're in the
>>>    privileged-tags (that's why I used the term "registered" in previous comments,
>>>    might be better suited if we deem that privileged is then confusing)
>>>
>>> - highlight the fact if a tag is in both
>>>
>> 
>> ok, then i have to change the permission checking code (currently i forbid
>> 'normal' users the tag if it was in the 'privileged-tags' section, regardless
>>  if it was in the 'user-allow-list' or not)
> 
> maybe wait on Fabian's opinion on that, I don't want to push this to strongly
> but can imagine that it might be sensible and useful, and hard to change later.

If we say vzdump should only use privileged tags for backup inclusion logic (to
avoid unprivileged users adding that tag to their VM and causing it to be backed
up), but then make some of those tags effectively non-privileged (which allows
exactly that), why do we have the restriction in vzdump in the first place?

that sounds like a complicated way (with lots of side-effects, because
privileged tags might be used in other places in the future as well) to make the
"vzdump should only use privileged tags" part configurable.. maybe there should
simply be a list of "vzdump tags" in addition to the privileged ones? those
would then be unprivileged, but the scope of "these allow vzdump job inclusion"
is clear and limited. or we just keep "vzdump only looks at privileged tags" for
now to keep it simple - extending that one way or another in the future is
always possible if it is restricted now, the other way is harder ;)




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  9:31             ` Fabian Grünbichler
@ 2022-11-16  9:38               ` Dominik Csapak
  2022-11-16  9:40               ` Thomas Lamprecht
  1 sibling, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-16  9:38 UTC (permalink / raw)
  To: Fabian Grünbichler, Proxmox VE development discussion,
	Thomas Lamprecht

On 11/16/22 10:31, Fabian Grünbichler wrote:
> On November 16, 2022 10:10 am, Thomas Lamprecht wrote:
>> Am 16/11/2022 um 10:04 schrieb Dominik Csapak:
>>> On 11/16/22 09:54, Thomas Lamprecht wrote:
>>>> Am 16/11/2022 um 09:47 schrieb Dominik Csapak:
>>>>>> I am not sure the second sentence is necessary, or rather, wouldn't it be better
>>>>>> to make the two lists mutually exclusive? e.g., by removing privileged tags from
>>>>>> the other list?
>>>>>
>>>>> i don't really want to auto remove stuff from one option when set on another.
>>>>> maybe it'd make more sense if we don't allow setting and admin tag when
>>>>> it's already set in the 'user-allow-list' and vice versa? then
>>>>> there cannot be a situation where a tag is in both lists at the same time?
>>>>>
>>>>
>>>>
>>>> Limits use cases, as we'll only ever allow priv'd tags to be used for things
>>>> like backup job guest-source selection, and there may be scenarios where an
>>>> admin wants to allow the user to set a specific privileged tags in the VMs
>>>> they control.
>>>>
>>>> To make that work we'd:
>>>> - explicitly allow such listed tags for "normal" VM users even if they're in the
>>>>     privileged-tags (that's why I used the term "registered" in previous comments,
>>>>     might be better suited if we deem that privileged is then confusing)
>>>>
>>>> - highlight the fact if a tag is in both
>>>>
>>>
>>> ok, then i have to change the permission checking code (currently i forbid
>>> 'normal' users the tag if it was in the 'privileged-tags' section, regardless
>>>   if it was in the 'user-allow-list' or not)
>>
>> maybe wait on Fabian's opinion on that, I don't want to push this to strongly
>> but can imagine that it might be sensible and useful, and hard to change later.
> 
> If we say vzdump should only use privileged tags for backup inclusion logic (to
> avoid unprivileged users adding that tag to their VM and causing it to be backed
> up), but then make some of those tags effectively non-privileged (which allows
> exactly that), why do we have the restriction in vzdump in the first place?
> 
> that sounds like a complicated way (with lots of side-effects, because
> privileged tags might be used in other places in the future as well) to make the
> "vzdump should only use privileged tags" part configurable.. maybe there should
> simply be a list of "vzdump tags" in addition to the privileged ones? those
> would then be unprivileged, but the scope of "these allow vzdump job inclusion"
> is clear and limited. or we just keep "vzdump only looks at privileged tags" for
> now to keep it simple - extending that one way or another in the future is
> always possible if it is restricted now, the other way is harder ;)

i think the reasoning of thomas here is that when we allow setting it in both
it's up to the admin how to interpret that for example:

we have x places where we only use privileged tags (e.g vzdump, migration, ...)
but now the admin wants to allow the other admins to add the tags
necessary for migration, but not for the others

then we'd have to add a seperate list for each occurance of privileged tags,
or we do it like thomas described then the admin simply sets in the
'user-allow-list' too

does that make sense?




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  9:31             ` Fabian Grünbichler
  2022-11-16  9:38               ` Dominik Csapak
@ 2022-11-16  9:40               ` Thomas Lamprecht
  2022-11-16  9:51                 ` Fabian Grünbichler
  1 sibling, 1 reply; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16  9:40 UTC (permalink / raw)
  To: Fabian Grünbichler, Dominik Csapak,
	Proxmox VE development discussion

Am 16/11/2022 um 10:31 schrieb Fabian Grünbichler:
>>> ok, then i have to change the permission checking code (currently i forbid
>>> 'normal' users the tag if it was in the 'privileged-tags' section, regardless
>>>  if it was in the 'user-allow-list' or not)
>> maybe wait on Fabian's opinion on that, I don't want to push this to strongly
>> but can imagine that it might be sensible and useful, and hard to change later.
> If we say vzdump should only use privileged tags for backup inclusion logic (to
> avoid unprivileged users adding that tag to their VM and causing it to be backed
> up), but then make some of those tags effectively non-privileged (which allows
> exactly that), why do we have the restriction in vzdump in the first place?

maybe re-read my scenario, feels like you're missing a bit here, maybe name it
"registered-tags" as suggested to make the confusion go away.

> 
> that sounds like a complicated way (with lots of side-effects, because

it's very simple?

> privileged tags might be used in other places in the future as well) to make the
> "vzdump should only use privileged tags" part configurable.. maybe there should
> simply be a list of "vzdump tags" in addition to the privileged ones? those
> would then be unprivileged, but the scope of "these allow vzdump job inclusion"
> is clear and limited. or we just keep "vzdump only looks at privileged tags" for
> now to keep it simple - extending that one way or another in the future is
> always possible if it is restricted now, the other way is harder 😉

not sure where you get complicated?

- You have a list of tags that are useable for backup source

- You have a mode where you can say that a list of tags that "normal VM admins" can use

- If they intersect then a "normal VM admin" can use it too

If you want to give a user control of what a (admin controlled!) job includes in
terms of guests then you can do so easily by also allowing the registered tag, if
not then don't? Note that not all setups host externally mostly untrusted guests/
users, the bigger market for us is those where a admin has a trust basis and also
no problem in giving control




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

* Re: [pve-devel] [PATCH cluster v10 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-11-16  9:50   ` Wolfgang Bumiller
  0 siblings, 0 replies; 44+ messages in thread
From: Wolfgang Bumiller @ 2022-11-16  9:50 UTC (permalink / raw)
  To: Dominik Csapak; +Cc: pve-devel

On Tue, Nov 15, 2022 at 02:02:26PM +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>
> ---

Sort-of
Acked-by: Wolfgang Bumiller <w.bumiller@proxmox.com>

but I do have a bit of a maintainability-complaint and potential actual
issue (see below).

> changes from v9:
> * line_len, prop_len -> size_t
> * document min/max
> * remove setting of null byte in loop (it's already split at the null byte)
> * improved the loop for skipping whitespace at the beginning
>  data/src/cfs-ipc-ops.h |   2 +
>  data/src/server.c      |  63 ++++++++++++++
>  data/src/status.c      | 184 ++++++++++++++++++++++++++++-------------
>  data/src/status.h      |   3 +
>  4 files changed, 194 insertions(+), 58 deletions(-)
> 
> diff --git a/data/src/status.c b/data/src/status.c
> index 9bceaeb..47cb42e 100644
> --- a/data/src/status.c
> +++ b/data/src/status.c
> @@ -804,25 +804,55 @@ 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

Please add:

// `line[line_len]` is guaranteed to be `\0`

keep reading for why ;-)

> +char *
> +_get_property_value_from_line(char *line, size_t line_len, const char *prop, size_t 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];
> +		char *v_end = &line[line_len - 1];

nit: in most C code an start and end pointers are exclusive on the end side
(iow. end should point to the first invalid address) it's a bit
confusing doing it this way and may lead to confusion...

> +
> +		// drop initial value whitespaces here already
> +		while (v_end > v_start && *v_start && isspace(*v_start)) v_start++;

(((that's fine, but I find 'v_start < v_end' much easier to parse)))

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

this is where the v_end tripped me up a bit:

- if there's no value, v_start == v_end and the loop won't run

> +		while (v_end > v_start && isspace(*v_end)) v_end--;
> +		v_end[1] = '\0';

which makes this read as "write to 1 past the end"

which *seems* fine at first given that we split the lines by replacing
'\n' with '\0', but then there's another case to think about:
The original data just came from a `memdb_read()` which just gives us a
"binary blob" and may not be newline-terminated.

However, the *caller* guarantees that in this case, we simply ignore the
final line.

BUT: do we do the same in perl?

> +
> +		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
> +//
> +// min..max is the char range of the first character of the given props,
> +// so that we can return early when checking the line
> +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;

^ I'd prefer an uint8_t here and call it `prop_count` as it's strictly
to count towards the above `uint8_t num_props`, so `int` makes little
sense, and the generic name `count` with type `uint8_t` is also a bit
awkward ;-)

>  
>  	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 +860,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;
> -
> -		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;
> +		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;
> +
> +		size_t line_len = strlen(line);

This is only fine because we skip the final line if it's not
`\n`-terminated (as it's otherwise not guaranteed to be `\0`-terminated)

But we don't even need to re-scan the line, since we already did that,
and should just be able to use:

    line_len = next_newline - line;

no?

> +		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




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  9:40               ` Thomas Lamprecht
@ 2022-11-16  9:51                 ` Fabian Grünbichler
  2022-11-16 13:56                   ` Thomas Lamprecht
  0 siblings, 1 reply; 44+ messages in thread
From: Fabian Grünbichler @ 2022-11-16  9:51 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox VE development discussion, Thomas Lamprecht

On November 16, 2022 10:40 am, Thomas Lamprecht wrote:
> Am 16/11/2022 um 10:31 schrieb Fabian Grünbichler:
>>>> ok, then i have to change the permission checking code (currently i forbid
>>>> 'normal' users the tag if it was in the 'privileged-tags' section, regardless
>>>>  if it was in the 'user-allow-list' or not)
>>> maybe wait on Fabian's opinion on that, I don't want to push this to strongly
>>> but can imagine that it might be sensible and useful, and hard to change later.
>> If we say vzdump should only use privileged tags for backup inclusion logic (to
>> avoid unprivileged users adding that tag to their VM and causing it to be backed
>> up), but then make some of those tags effectively non-privileged (which allows
>> exactly that), why do we have the restriction in vzdump in the first place?
> 
> maybe re-read my scenario, feels like you're missing a bit here, maybe name it
> "registered-tags" as suggested to make the confusion go away.
> 
>> 
>> that sounds like a complicated way (with lots of side-effects, because
> 
> it's very simple?
> 
>> privileged tags might be used in other places in the future as well) to make the
>> "vzdump should only use privileged tags" part configurable.. maybe there should
>> simply be a list of "vzdump tags" in addition to the privileged ones? those
>> would then be unprivileged, but the scope of "these allow vzdump job inclusion"
>> is clear and limited. or we just keep "vzdump only looks at privileged tags" for
>> now to keep it simple - extending that one way or another in the future is
>> always possible if it is restricted now, the other way is harder 😉
> 
> not sure where you get complicated?
> 
> - You have a list of tags that are useable for backup source
>

the problem is that this list of tags might also be used for other things than
backup source (either by PVE, or by custom tooling) where the difference between
(who can set a) "regular tag" and (a) "privileged/registered/.. tag" matters.

> - You have a mode where you can say that a list of tags that "normal VM admins" can use
> 
> - If they intersect then a "normal VM admin" can use it too
> 
> If you want to give a user control of what a (admin controlled!) job includes in
> terms of guests then you can do so easily by also allowing the registered tag, if
> not then don't? Note that not all setups host externally mostly untrusted guests/
> users, the bigger market for us is those where a admin has a trust basis and also
> no problem in giving control

I understand the issue/scenario, but I think the missing scope restricts us down
the line when we want to start using the difference between "registered"
(restricted?) tags and normal ones for other things besides vzdump - because the
admin when lifting the restriction might not be aware of the implication that
this doesn't just affect vzdump jobs (for example, because the other feature
that also uses the list of special tags is not even implemented yet at that
point). if we don't care about that then sure, we can just have two lists and
allow tags being in both, but it should come with a warning about the
implications ;)




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

* [pve-devel] applied: [PATCH widget-toolkit v10 1/2] add tag related helpers
  2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 1/2] add tag related helpers Dominik Csapak
@ 2022-11-16 13:48   ` Thomas Lamprecht
  0 siblings, 0 replies; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16 13:48 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

Am 15/11/2022 um 14:02 schrieb Dominik Csapak:
> 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>
> ---
> changes from v9:
> * tags are now 'inline-block' and for boundlists have a line-height
>   of 15px (fixes an overflow issue)
>  src/Utils.js         | 88 ++++++++++++++++++++++++++++++++++++++++++++
>  src/css/ext6-pmx.css | 52 ++++++++++++++++++++++++++
>  2 files changed, 140 insertions(+)
> 
>

applied, thanks!




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

* [pve-devel] applied: [PATCH widget-toolkit v10 2/2] Toolkit: add override for Ext.dd.DragDropManager
  2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
@ 2022-11-16 13:49   ` Thomas Lamprecht
  0 siblings, 0 replies; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16 13:49 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

Am 15/11/2022 um 14:02 schrieb Dominik Csapak:
> 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(+)
> 
>

applied, thanks!




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

* Re: [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16  9:51                 ` Fabian Grünbichler
@ 2022-11-16 13:56                   ` Thomas Lamprecht
  0 siblings, 0 replies; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16 13:56 UTC (permalink / raw)
  To: Proxmox VE development discussion, Fabian Grünbichler,
	Dominik Csapak

Am 16/11/2022 um 10:51 schrieb Fabian Grünbichler:
>> - You have a list of tags that are useable for backup source
>>
> the problem is that this list of tags might also be used for other things than
> backup source (either by PVE, or by custom tooling) where the difference between
> (who can set a) "regular tag" and (a) "privileged/registered/.. tag" matters.
> 
>> - You have a mode where you can say that a list of tags that "normal VM admins" can use
>>
>> - If they intersect then a "normal VM admin" can use it too
>>
>> If you want to give a user control of what a (admin controlled!) job includes in
>> terms of guests then you can do so easily by also allowing the registered tag, if
>> not then don't? Note that not all setups host externally mostly untrusted guests/
>> users, the bigger market for us is those where a admin has a trust basis and also
>> no problem in giving control
> I understand the issue/scenario, but I think the missing scope restricts us down
> the line when we want to start using the difference between "registered"
> (restricted?) tags and normal ones for other things besides vzdump - because the
> admin when lifting the restriction might not be aware of the implication that
> this doesn't just affect vzdump jobs (for example, because the other feature
> that also uses the list of special tags is not even implemented yet at that
> point). if we don't care about that then sure, we can just have two lists and
> allow tags being in both, but it should come with a warning about the
> implications 😉

meh, that is somewhat true for a lot of feature expansions, but yes, we don't have
any such feature (as we need tags to work first -> chicken/egg), and deciding that
stuff now for the future is probably always missing some things; so I recommended
Dominik off-list to go for the "require / Sys.Modify" approach for now and ideally
add a frontend warning/hint if there are shared ones between the registered/privileged
and user allow-list. We can then expand on this when actually implementing features
and getting user feedback, that is def. safer for now.




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

* Re: [pve-devel] [PATCH manager v10 08/13] ui: add form/Tag
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 08/13] ui: add form/Tag Dominik Csapak
@ 2022-11-16 14:57   ` Thomas Lamprecht
  0 siblings, 0 replies; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16 14:57 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

For the subject: s!form/Tag!Proxmox.form.Tag component!

Am 15/11/2022 um 14:02 schrieb Dominik Csapak:
> +Ext.define('Proxmox.Tag', {

Would define it as 'Proxmox.form.Tag' matching the file hierarchy.


> +    extend: 'Ext.Component',
> +    alias: 'widget.pmxTag',

it's not wrong but it feels like there should be something more added to the
widget alias, but lacking a significantly better proposal tbh., pmxTagPicker
pmxTagDisplayEdit feel not really good either and it def. isn't a issue and
can stay as is too.






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

* Re: [pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js
  2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js Dominik Csapak
@ 2022-11-16 15:00   ` Thomas Lamprecht
  2022-11-16 15:02     ` Dominik Csapak
  0 siblings, 1 reply; 44+ messages in thread
From: Thomas Lamprecht @ 2022-11-16 15:00 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

subject -> s!form/TagEdit.js!PVE.panel.TagEditContainer component!

Am 15/11/2022 um 14:02 schrieb Dominik Csapak:
> +Ext.define('PVE.panel.TagEditContainer', {
> +    extend: 'Ext.container.Container',
> +    alias: 'widget.pveTagEditContainer',

nit: slightly odd that this is pveABC while in the previous patch you used
pmxABC, I guess the former lifted in widget-toolkit initially ^^






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

* Re: [pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js
  2022-11-16 15:00   ` Thomas Lamprecht
@ 2022-11-16 15:02     ` Dominik Csapak
  0 siblings, 0 replies; 44+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:02 UTC (permalink / raw)
  To: Thomas Lamprecht, Proxmox VE development discussion

On 11/16/22 16:00, Thomas Lamprecht wrote:
> subject -> s!form/TagEdit.js!PVE.panel.TagEditContainer component!
> 
> Am 15/11/2022 um 14:02 schrieb Dominik Csapak:
>> +Ext.define('PVE.panel.TagEditContainer', {
>> +    extend: 'Ext.container.Container',
>> +    alias: 'widget.pveTagEditContainer',
> 
> nit: slightly odd that this is pveABC while in the previous patch you used
> pmxABC, I guess the former lifted in widget-toolkit initially ^^
> 
> 


you're right, i'll change the pmxTag to pveTag since it lives in pve currently anyway...




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

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

Thread overview: 44+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-11-15 13:02 [pve-devel] [PATCH cluster/guest-common/qemu-server/container/wt/manager v10 0/5] add tags to ui Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
2022-11-16  9:50   ` Wolfgang Bumiller
2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 2/5] Cluster: add get_guest_config_properties Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 3/5] datacenter.cfg: add option for tag-style Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 4/5] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
2022-11-15 15:17   ` Fabian Grünbichler
2022-11-16  7:48     ` Thomas Lamprecht
2022-11-16  8:47     ` Dominik Csapak
2022-11-16  8:51       ` Fabian Grünbichler
2022-11-16  8:54       ` Thomas Lamprecht
2022-11-16  9:04         ` Dominik Csapak
2022-11-16  9:10           ` Thomas Lamprecht
2022-11-16  9:31             ` Fabian Grünbichler
2022-11-16  9:38               ` Dominik Csapak
2022-11-16  9:40               ` Thomas Lamprecht
2022-11-16  9:51                 ` Fabian Grünbichler
2022-11-16 13:56                   ` Thomas Lamprecht
2022-11-15 13:02 ` [pve-devel] [PATCH cluster v10 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH guest-common v10 1/1] GuestHelpers: add 'assert_tag_permissions' Dominik Csapak
2022-11-15 15:34   ` Fabian Grünbichler
2022-11-15 13:02 ` [pve-devel] [PATCH qemu-server v10 1/1] api: update: check for tags permissions with 'assert_tag_permissions' Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH container v10 1/1] check_ct_modify_config_perm: " Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 1/2] add tag related helpers Dominik Csapak
2022-11-16 13:48   ` [pve-devel] applied: " Thomas Lamprecht
2022-11-15 13:02 ` [pve-devel] [PATCH widget-toolkit v10 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
2022-11-16 13:49   ` [pve-devel] applied: " Thomas Lamprecht
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 01/13] api: /cluster/resources: add tags to returned properties Dominik Csapak
2022-11-16  8:02   ` Thomas Lamprecht
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 02/13] api: add /ui-options api call Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 03/13] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 04/13] ui: parse and save tag infos from /ui-options Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 05/13] ui: add form/TagColorGrid Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 06/13] ui: add PVE.form.ListField Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 07/13] ui: dc/OptionView: add editors for tag settings Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 08/13] ui: add form/Tag Dominik Csapak
2022-11-16 14:57   ` Thomas Lamprecht
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js Dominik Csapak
2022-11-16 15:00   ` Thomas Lamprecht
2022-11-16 15:02     ` Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 11/13] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 12/13] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
2022-11-15 13:02 ` [pve-devel] [PATCH manager v10 13/13] ui: implement tag ordering from datacenter.cfg Dominik Csapak

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