You can use the filter regex_findall to fit your condition, and accompany it with slicing, to limit the length of the string.
Then, to reduce the string to the acceptable pattern, you could do:
- set_fact:
    vm_name: "{{ (vm_name | regex_findall('[A-Za-z0-9-_]'))[:15] | join }}"
  vars:
    vm_name: )a&bc"d#efgh12_12-0(22
Which gives something like
ok: [localhost] => changed=false 
  ansible_facts:
    vm_name: abcdefgh12_12-0
Note: the above output have been generated running the playbook with the option -v, which, amongst other useful information, shows the result of a set_fact task.
Or, if you want to fail:
- assert:
    ## By testing the length before and after the regex filtering
    ## and the slicing, we assert if the filtering did alter the 
    ## string length, and so, the fact that the original string
    ## did contain invalid character or was too long
    that: "(
        vm_name | regex_findall('[A-Za-z0-9-_]')
      )[:15] | join | length == vm_name | length"
  vars:
    vm_name: abcdefgh12_12-0