Repository: ansible-collections/community.general

Issue:
Allow tags to be supplied to proxmox_kvm on QEMU VM create
### Summary

I checked the parameter list defined at https://github.com/ansible-collections/community.general/blob/36daa7c48e84cb92f4b908cc043bf0edbc95f162/plugins/modules/cloud/misc/proxmox_kvm.py#L992 and per the documentation, the `tags` is not a parameter that can be passed via `community.general.proxmox_kvm` module.

It is available in the Proxmox VE API - https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/qemu/{vmid}/config - and seems to be available under `POST` and `PUT`. I understand this uses Proxmoxer ( https://github.com/proxmoxer/proxmoxer ) but my understanding is that it is a wrapper and should pass the parameter if sent from `proxmox_kvm`.

### Issue Type

Feature Idea

### Component Name

proxmox_kvm

### Additional Information

Example use:
```yaml (paste below)
- name: Create new VM with minimal options (with tags)
  community.general.proxmox_kvm:
    api_user: root@pam
    api_password: secret
    api_host: helldorado
    name: spynal
    node: sabrewulf
    tags: "tag1"

```


### Code of Conduct

- [x] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/cloud/misc/proxmox_kvm.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/misc/proxmox_kvm.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Aversiste @Thulium-Drake @helldorado @joshainglis @karmab
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I recently submitted PR #1949 to fix the handling of tags strings in the inventory plugin and add an additional parsed list as a fact.
Some words of caution:
- This is a version 6 only feature
- This should work with QEMU, but should not be extended to lxc currently (See https://forum.proxmox.com/threads/api-parse-errors-when-lxc-tags-are-null.85197/)
- I have yet to find a reference for the allowed character set for the tags string, but some obvious characters are excluded ('=',':')

The last point is the only concerning one since I envision someone with a create task that works in every respect except for an improper tags string which will cause them much frustration.

If the proxmox team can supply a spec for the tags string and that validation is put upfront in this module to better inform the user then this should be trivial to implement.
@Ajpantuso I found the following snippet in the current HEAD of the [proxmox/pve-common](https://github.com/proxmox/pve-common/blob/30ca0de57a624d81c522a407c5519d1f113888c1/src/PVE/JSONSchema.pm#L630) defining `pve-tag`:

```perl
# used for pve-tag-list in e.g., guest configs
register_format('pve-tag', \&pve_verify_tag);
sub pve_verify_tag {
    my ($value, $noerr) = @_;

    return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i;

    return undef if $noerr;

    die "invalid characters in tag\n";
}
```
The said `pve-tag-list` is used in [proxmox/qemu-server](https://github.com/proxmox/qemu-server/blob/31c8e14bf29b5e2f91bf7130b9d0285b90bbe439/PVE/QemuServer.pm#L674) :

```perl
    tags => {
	type => 'string', format => 'pve-tag-list',
	description => 'Tags of the VM. This is only meta information.',
	optional => 1,
    },
```
But I could not locate the `pve-tag-list` definition. Comma separated would seem to be the case, with each `pve-tag` defined by the RegEx in `pve-common`.
> @Ajpantuso I found the following snippet in the current HEAD of the [proxmox/pve-common](https://github.com/proxmox/pve-common/blob/30ca0de57a624d81c522a407c5519d1f113888c1/src/PVE/JSONSchema.pm#L630) defining `pve-tag`:
> 
> ```perl
> # used for pve-tag-list in e.g., guest configs
> register_format('pve-tag', \&pve_verify_tag);
> sub pve_verify_tag {
>     my ($value, $noerr) = @_;
> 
>     return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i;
> 
>     return undef if $noerr;
> 
>     die "invalid characters in tag\n";
> }
> ```
> 
> The said `pve-tag-list` is used in [proxmox/qemu-server](https://github.com/proxmox/qemu-server/blob/31c8e14bf29b5e2f91bf7130b9d0285b90bbe439/PVE/QemuServer.pm#L674) :
> 
> ```perl
>     tags => {
> 	type => 'string', format => 'pve-tag-list',
> 	description => 'Tags of the VM. This is only meta information.',
> 	optional => 1,
>     },
> ```
> 
> But I could not locate the `pve-tag-list` definition. Comma separated would seem to be the case, with each `pve-tag` defined by the RegEx in `pve-common`.

Ah thank you.

Not fun digging through their perl modules, but from what I see appending "-list" to a format name will trigger value parsing:

```perl
sub check_format {
    my ($format, $value, $path) = @_;

    if (ref($format) eq 'HASH') {
	# hash ref cannot have validator/list/opt handling attached
	return parse_property_string($format, $value, $path);
    }

    if (ref($format) eq 'CODE') {
	# we are the (sole, old-style) validator
	return $format->($value);
    }

    return if $format eq 'regex';

    my $parsed;
    $format =~ m/^(.*?)(?:-a?(list|opt))?$/;
    my ($format_name, $format_type) = ($1, $2 // 'none');
    my $registered = get_format($format_name);
    die "undefined format '$format'\n" if !$registered;

    die "'-$format_type' format must have code ref, not hash\n"
	if $format_type ne 'none' && ref($registered) ne 'CODE';

    if ($format_type eq 'list') {
	# Note: we allow empty lists
	foreach my $v (split_list($value)) {
	    $parsed = $registered->($v);
	}
    } elsif ($format_type eq 'opt') {
	$parsed = $registered->($value) if $value;
   } else {
	if (ref($registered) eq 'HASH') {
	    # Note: this is the only case where a validator function could be
	    # attached, hence it's safe to handle that in parse_property_string.
	    # We do however have to call it with $format_name instead of
	    # $registered, so it knows about the name (and thus any validators).
	    $parsed = parse_property_string($format, $value, $path);
	} else {
	    $parsed = $registered->($value);
	}
    }

    return $parsed;
}
```


And the split_list helper comes from Tools.pm which looks like it allows either ',' or ';' as a seperator.

```perl
sub split_list {
    my $listtxt = shift // '';

    return split (/\0/, $listtxt) if $listtxt =~ m/\0/;

    $listtxt =~ s/[,;]/ /g;
    $listtxt =~ s/^\s+//;

    my @data = split (/\s+/, $listtxt);

    return @data;
}

```
👍 Nice find!

So if we do end up adding the `tags` to the `community.general.proxmox_kvm` module, then we should specify for the parameter that:

- tags available only in Proxmox 6+
- tags should be comma or semi-colon separated
- tags can contain a-z or 0-9 or _ or - or + or .
- tags should start with a-z or 0-9 or _

Is the documentation for the modules in this `asnible-collections/community.general` same repository? The 'Edit on Github' link in the documentation site seems broken - https://docs.ansible.com/ansible/latest/collections/community/general/
> Is the documentation for the modules in this `asnible-collections/community.general` same repository?

Yes. The documentation is auto-generated from the modules and plugins.

> The 'Edit on Github' link in the documentation site seems broken - https://docs.ansible.com/ansible/latest/collections/community/general/

The auto-generated index pages should not have that link - that's a bug. I've passed that on.
> 👍 Nice find!
> 
> So if we do end up adding the `tags` to the `community.general.proxmox_kvm` module, then we should specify for the parameter that:
> 
> * tags available only in Proxmox 6+
> * tags should be comma or semi-colon separated
> * tags can contain a-z or 0-9 or _ or - or + or .
> * tags should start with a-z or 0-9 or _
> 
> Is the documentation for the modules in this `asnible-collections/community.general` same repository? The 'Edit on Github' link in the documentation site seems broken - https://docs.ansible.com/ansible/latest/collections/community/general/

Yes, one suggestion however is that instead of asking for the raw tags string in the parameter it would make sense to accept a yaml list of tags. (Seems more straightforward to work with from an ansible perspective)

Example:

```yaml
tags:
  - tag1
  - tag2
  - tag3
```

or

```yaml
tags: ['tag1', 'tag2', 'tag3']
```

The module would then validate each tag and join them with commas or semi-colons to pass on to proxmoxer.

I would also suggest being strict and failing the task if tags are invalid. (Allowing the task to proceed without complete tags may cause unexpected results if the tags are used in the same playbook or sequence of plays)

Resolving patch:
diff --git a/plugins/modules/cloud/misc/proxmox_kvm.py b/plugins/modules/cloud/misc/proxmox_kvm.py
--- a/plugins/modules/cloud/misc/proxmox_kvm.py
+++ b/plugins/modules/cloud/misc/proxmox_kvm.py
@@ -425,6 +425,14 @@
         option has a default of C(no). Note that the default value of I(proxmox_default_behavior)
         changes in community.general 4.0.0.
     type: bool
+  tags:
+    description:
+      - List of tags to apply to the VM instance.
+      - Tags must start with C([a-z0-9_]) followed by zero or more of the following characters C([a-z0-9_-+.]).
+      - Tags are only available in Proxmox 6+.
+    type: list
+    elements: str
+    version_added: 2.3.0
   target:
     description:
       - Target node. Only allowed if the original VM is on shared storage.
@@ -858,7 +866,7 @@ def wait_for_task(module, proxmox, node, taskid):
 def create_vm(module, proxmox, vmid, newid, node, name, memory, cpu, cores, sockets, update, **kwargs):
     # Available only in PVE 4
     only_v4 = ['force', 'protection', 'skiplock']
-    only_v6 = ['ciuser', 'cipassword', 'sshkeys', 'ipconfig']
+    only_v6 = ['ciuser', 'cipassword', 'sshkeys', 'ipconfig', 'tags']
 
     # valide clone parameters
     valid_clone_params = ['format', 'full', 'pool', 'snapname', 'storage', 'target']
@@ -928,6 +936,13 @@ def create_vm(module, proxmox, vmid, newid, node, name, memory, cpu, cores, sock
         if searchdomains:
             kwargs['searchdomain'] = ' '.join(searchdomains)
 
+    # VM tags are expected to be valid and presented as a comma/semi-colon delimited string
+    if 'tags' in kwargs:
+        for tag in kwargs['tags']:
+            if not re.match(r'^[a-z0-9_][a-z0-9_\-\+\.]*$', tag):
+                module.fail_json(msg='%s is not a valid tag' % tag)
+        kwargs['tags'] = ",".join(kwargs['tags'])
+
     # -args and skiplock require root@pam user - but can not use api tokens
     if module.params['api_user'] == "root@pam" and module.params['args'] is None:
         if not update and module.params['proxmox_default_behavior'] == 'compatibility':
@@ -1063,6 +1078,7 @@ def main():
             state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted', 'current']),
             storage=dict(type='str'),
             tablet=dict(type='bool'),
+            tags=dict(type='list', elements='str'),
             target=dict(type='str'),
             tdf=dict(type='bool'),
             template=dict(type='bool'),
@@ -1267,6 +1283,7 @@ def main():
                       startdate=module.params['startdate'],
                       startup=module.params['startup'],
                       tablet=module.params['tablet'],
+                      tags=module.params['tags'],
                       target=module.params['target'],
                       tdf=module.params['tdf'],
                       template=module.params['template'],


Test patch: