├── library ├── __init__.py ├── pytest.ini ├── pvesh.md ├── test_proxmox_prov.py ├── conftest.py └── proxmox_prov.py ├── tests ├── inventory └── test.yml ├── vars └── main.yml ├── handlers └── main.yml ├── meta └── main.yml ├── tasks ├── templates.yml ├── main.yml ├── host.yml ├── permission.yml ├── storage.yml └── prov.yml ├── LICENSE ├── .gitignore ├── defaults └── main.yml ├── examples └── containers.yml └── README.md /library/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | 3 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for . -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for . -------------------------------------------------------------------------------- /tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | remote_user: root 4 | roles: 5 | - . -------------------------------------------------------------------------------- /library/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | color=yes 3 | # addopts=-sv 4 | # addopts=-v -r a 5 | #-r chars show extra test summary info as specified by chars 6 | # (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed 7 | # (w)pytest-warnings (p)passed, (P)passed with output, 8 | # (a)all except pP. 9 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: mozit.at 3 | description: > 4 | Configure proxmox hosts and lxc containers without dependencies. 5 | This role assumes proxmox is already installed on the host system. 6 | company: mozit.at 7 | license: MIT 8 | min_ansible_version: 2.0 9 | platforms: 10 | - name: Debian 11 | versions: 12 | - wheezy 13 | - jessie 14 | - bookworm 15 | 16 | galaxy_tags: 17 | - proxmox 18 | - system 19 | - lxc 20 | # - debian 21 | 22 | dependencies: [] 23 | -------------------------------------------------------------------------------- /tasks/templates.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Templates > update PVE template list 4 | command: pveam update 5 | when: proxmoxy__templates_update 6 | 7 | - name: Templates > find available 8 | shell: pveam available | awk '{print $2}' 9 | changed_when: false 10 | register: __proxmoxy_templates_all_list 11 | 12 | - name: Templates > filter available list 13 | set_fact: 14 | __proxmoxy_templates_list: "{{ __proxmoxy_templates_list|default([]) + __proxmoxy_templates_all_list.stdout_lines|select('search', item)|sort }}" 15 | # changed_when: false 16 | with_items: "{{ proxmoxy__templates }}" 17 | when: not ansible_check_mode 18 | 19 | - name: Templates > download matching templates 20 | shell: > 21 | pveam download {{ proxmoxy__templates_storage|default('local') }} {{ item }} 22 | creates={{ proxmoxy__templates_dir }}/{{ item }} 23 | with_items: "{{ __proxmoxy_templates_list | default([]) }}" -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Main > get proxmox version 4 | shell: pveversion | cut -d'/' -f2 5 | register: __proxmox_version 6 | changed_when: false 7 | 8 | # assume 6.0 if cannot determine version (i.e. in check mode) 9 | - set_fact: 10 | proxmoxy__pve_version: "{{ __proxmox_version.stdout if __proxmox_version.stdout is 11 | defined else '6.0' }}" 12 | # cacheable: yes 13 | 14 | # Host system 15 | - ansible.builtin.include_tasks: host.yml 16 | tags: 17 | - proxmoxy 18 | 19 | # Permissions 20 | - ansible.builtin.include_tasks: permission.yml 21 | tags: 22 | - proxmoxy 23 | 24 | # Storage 25 | - ansible.builtin.include_tasks: storage.yml 26 | tags: 27 | - proxmoxy 28 | 29 | # Templates 30 | - ansible.builtin.include_tasks: templates.yml 31 | tags: 32 | - proxmoxy 33 | 34 | # Provisioning 35 | - ansible.builtin.include_tasks: prov.yml 36 | tags: 37 | - proxmoxy 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mozit.eu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .pytest_cache/ 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # KDE dolphin 93 | .directory 94 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ## Host system 4 | proxmoxy__host_modules: {} 5 | proxmoxy__host_tuntap: True # enable tun/tap for lxc CTs. 6 | proxmoxy__host_remove_nosubnag: True # remove no subscription login message. 7 | proxmoxy__host_repo_enterprise: False # remove enterprise apt repo if false 8 | proxmoxy__host_repo_nosubs: True # enable no-subscription repo 9 | 10 | ## Templates 11 | proxmoxy__templates_storage: "local" 12 | proxmoxy__templates_dir: "/var/lib/vz/template/cache" 13 | # can use regex pattern 14 | proxmoxy__templates_default: "centos-7" 15 | # Should pveam update be called always? 16 | proxmoxy__templates_update: False 17 | # pveam download templates 18 | proxmoxy__templates: [] 19 | 20 | ## Permissions, Users, ... 21 | proxmoxy__permission_groups: [{}] 22 | proxmoxy__permission_users: [{}] # uid user@pam must exist on linux host. 23 | proxmoxy__permission_roles: [{}] 24 | proxmoxy__permission_acls: [{}] 25 | 26 | ## Proxmox storage 27 | proxmoxy__storage: [{}] 28 | proxmoxy__storage_content: ['images', 'rootdir'] 29 | proxmoxy__storage_nodes: [] 30 | proxmoxy__storage_changes: True # Always set options, or don't 31 | proxmoxy__storage_remove: [] # storages to remove. Place here to recreate anew. 32 | 33 | ## Provision lxc containers 34 | proxmoxy__provision_secret: True # read/save password to from folder "secret/credentials//root/pw" 35 | proxmoxy__provision_bridge: 'vmbr0' 36 | proxmoxy__provision_containers: [] # list of container configs 37 | proxmoxy__provision_post_cmds: [] # run commands in new containers 38 | proxmoxy__provision_bootstrap_file: 'hosts/bootstrap' # if !="" write new containers into this file 39 | proxmoxy__provision_bootstrap_entry: 'bootstrap_hosts' # inventory entry name 40 | proxmoxy__provision_config_path: '/etc/pve/local/lxc' 41 | proxmoxy__provision_idmapping: True 42 | # will map vm ids 0-1999 to 100000-101999 (on host) 43 | proxmoxy__provision_idmap_from: 0 44 | # should match values in /etc/subuid 45 | proxmoxy__provision_idmap_start: 100000 46 | proxmoxy__provision_idmap_range: 65536 47 | -------------------------------------------------------------------------------- /examples/containers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ### Example of container with all the options possible 4 | proxmoxy__provision_containers: 5 | - vmid: 909 6 | state: present 7 | password: abc123 8 | hostname: supervm01 9 | ostemplate: 'centos7.*' 10 | # should be sorted from here on. 11 | arch: amd64 12 | cmode: tty 13 | console: 1 14 | cores: 2 15 | cpuunits: 1025 16 | description: All possible params on this CT are set via ansible. 17 | memory: 768 18 | # set some but not all mountpoints. 19 | mp0: 20 | storage: tank_multi 21 | volume: subvol-207-mp0 22 | mp: /ct_mount_mp0 23 | size: 4G 24 | acl: True 25 | mp2: 26 | storage: tank_multi 27 | volume: subvol-207-mp2 28 | mp: /ct_mount_mp2 29 | size: 5G 30 | acl: True 31 | # Dir mount, should not be created, dir must exist, size must be 0T 32 | mp8: 33 | volume: /mnt/dirvolume 34 | mp: /ct_mount_mp8 35 | size: 0T 36 | # Without size its a bind mount (not listed by df inside CT) 37 | mp9: 38 | volume: /mnt/bindmount 39 | mp: /ct_mount_mp9 40 | nameserver: 10.0.0.4 10.0.0.3 41 | # For netX: enclose int values in quotes: tag, mtu 42 | net0: 43 | name: eth0 44 | ip: 10.0.0.10/24 45 | gw: 10.0.0.1 46 | net1: 47 | name: eth1 48 | bridge: vmbr0 49 | ip: 10.0.0.11/24 50 | gw: 10.0.0.1 51 | hwaddr: B6:75:39:CC:46:B1 52 | tag: '4' 53 | mtu: '1420' 54 | type: veth 55 | node: proxmoxxy 56 | onboot: False 57 | ostype: centos 58 | # pool is not saved inside vmid.cfg and cannot be set with pvesh config, only create. 59 | pool: mytestpool 60 | protection: False 61 | rootfs: 62 | storage: local-zfs 63 | size: 7G 64 | searchdomain: domain.local 65 | ssh_public_keys: ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAgEAl7DTLWEoWutlNqF/IamAlooooongTestKeY... 66 | startup: 9 67 | storage: local-zfs # unnecessary if defined in rootfs 68 | swap: 256 69 | # template: False # does not seem to be implemented yet 70 | # timeout: 30 # only for shutdown ops. 71 | tty: 3 72 | unprivileged: True 73 | -------------------------------------------------------------------------------- /tasks/host.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ## Configure misc stuff on the proxmox host system 4 | 5 | # proxmox disable enterprise repo 6 | - name: Host > disable/enable proxmox enterprise repo 7 | apt_repository: 8 | repo: deb https://enterprise.proxmox.com/debian/pve {{ ansible_distribution_release }} pve-enterprise 9 | state: "{{ 'present' if proxmoxy__host_repo_enterprise else 'absent' }}" 10 | filename: pve-enterprise.list 11 | - name: Host > disable/enable proxmox enterprise ceph repo 12 | apt_repository: 13 | repo: deb https://enterprise.proxmox.com/debian/ceph-squid {{ ansible_distribution_release }} enterprise 14 | state: "{{ 'present' if proxmoxy__host_repo_enterprise else 'absent' }}" 15 | filename: ceph 16 | 17 | - name: enable/disable proxmox no-subscription repo 18 | apt_repository: 19 | repo: deb http://download.proxmox.com/debian/pve {{ ansible_distribution_release }} pve-no-subscription 20 | state: "{{ 'present' if proxmoxy__host_repo_nosubs else 'absent' }}" 21 | filename: pve-no-subscription 22 | - name: enable/disable proxmox no-subscription ceph repo 23 | apt_repository: 24 | repo: deb http://download.proxmox.com/debian/ceph-squid {{ ansible_distribution_release }} no-subscription 25 | state: "{{ 'present' if proxmoxy__host_repo_nosubs else 'absent' }}" 26 | filename: ceph 27 | 28 | # suggestion for firewall 29 | # - 'xt_tcpudp' 30 | # - 'xt_multiport' 31 | - name: Host > load additional kernel modules at boot 32 | lineinfile: 33 | dest: /etc/modules-load.d/{{ item.key }}.conf 34 | regexp: "^{{ item.value }}" 35 | line: "{{ item.value }}" 36 | state: present 37 | create: yes 38 | with_dict: "{{ proxmoxy__host_modules }}" 39 | 40 | - name: Host > modprobe kernel modules now 41 | modprobe: 42 | name: "{{ item.value }}" 43 | with_dict: "{{ proxmoxy__host_modules }}" 44 | 45 | - name: Host > remove proxmox ui no-subscription warning message v1 46 | lineinfile: 47 | # not sure about version 5 48 | path: "{{ '/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js' if proxmoxy__pve_version is 49 | version('5', '>=') else '/usr/share/pve-manager/ext6/pvemanagerlib.js' }}" 50 | regexp: "(\\s*if \\(data.status !== 'Active')(\\) {)" 51 | line: '\1 && false\2' 52 | backrefs: yes 53 | when: proxmoxy__host_remove_nosubnag 54 | 55 | # For version 7(.4 ?) 56 | - name: Host > remove proxmox ui no-subscription warning message v2 57 | ansible.builtin.replace: 58 | path: /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 59 | regexp: '\sExt.Msg.show\({' 60 | before: 'title: gettext\(.No valid subscription.\)' 61 | replace: ' void({ //Ext.Msg.show({' 62 | when: proxmoxy__host_remove_nosubnag 63 | 64 | # tbd lxc.mount.entry = /dev/net dev/net none bind,create=dir 65 | - name: Host > enable tun/tap device in lxc 66 | lineinfile: 67 | dest: /usr/share/lxc/config/common.conf.d/20-tuntap.conf 68 | regexp: "^lxc.cgroup.devices.allow" 69 | line: "lxc.cgroup.devices.allow = c 10:200 rwm" 70 | create: yes 71 | state: present 72 | when: proxmoxy__host_tuntap 73 | -------------------------------------------------------------------------------- /tasks/permission.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Manage Permissions, Groups, Users 4 | - name: Permission > get user config from file 5 | command: cat /etc/pve/user.cfg 6 | register: __proxmoxy_users 7 | changed_when: false 8 | 9 | # Kind of hackyish solution for differentiating between groupmod/groupadd 10 | - name: Permission > create/modify groups 11 | shell: > 12 | cmd=groupadd; if grep -q "^group:{{ item.name }}" /etc/pve/user.cfg; then cmd=groupmod; fi; 13 | pveum $cmd {{ item.name }} 14 | {{ (' -comment "' ~ item.comment + '"') if item.comment is defined else '' }} 15 | with_items: "{{ proxmoxy__permission_groups }}" 16 | when: item and not __proxmoxy_users.stdout | regex_search("group:" + item.name|default(omit) + ":[^:]*:" + item.comment|default('[^:]') + ":") 17 | 18 | # Better solution for useradd/usermod problem. 19 | - name: Permission > create/modify users 20 | shell: > 21 | pveum {{ 'usermod' if 'user:' ~ item.name in __proxmoxy_users.stdout else 'useradd' }} {{ item.name }} 22 | {{ (' -comment "' ~ item.comment ~ '"') if item.comment is defined else '' }} 23 | {{ (' -email ' ~ item.email) if item.email is defined else '' }} 24 | {{ (' -enable ' ~ item.enable) if item.enable is defined else '' }} 25 | {{ (' -expire ' ~ item.expire) if item.expire is defined else '' }} 26 | {{ (' -firstname "' ~ item.firstname ~ '"') if item.firstname is defined else '' }} 27 | {{ (' -groups "' ~ item.groups ~ '"') if item.groups is defined else '' }} 28 | {{ (' -keys "' ~ item.key ~ '"') if item.key is defined else '' }} 29 | {{ (' -lastname "' ~ item.lastname ~ '"') if item.lastname is defined else '' }} 30 | {{ (' -password "' ~ item.password ~ '"') if item.password is defined and 'user:' ~ item.name not in __proxmoxy_users.stdout else '' }} 31 | with_items: "{{ proxmoxy__permission_users }}" 32 | when: > 33 | item and not __proxmoxy_users.stdout | 34 | regex_search( "user:{0}:{1}:{2}:{3}:{4}:{5}:{6}:{7}:".format( 35 | item.name|default(omit), item.enable|int|default(1), 36 | item.expire|int|default(0), item.firstname|default('[^:]*'), item.lastname|default('[^:]*'), 37 | item.email|default('[^:]*'), item.comment|default('[^:]*'), item.key|default('[^:]*') 38 | ) ) 39 | 40 | - name: Permission > create/modify roles 41 | shell: > 42 | pveum {{ 'rolemod' if 'role:' ~ item.name in __proxmoxy_users.stdout else 'roleadd' }} {{ item.name }} 43 | {{ (' -privs "' ~ item.privs|sort|join(',') ~ '"') if item.privs is defined else '' }} 44 | with_items: "{{ proxmoxy__permission_roles }}" 45 | when: > 46 | item and not __proxmoxy_users.stdout | 47 | regex_search( "role:{0}:{1}:".format( 48 | item.name|default(omit), item.privs|sort|join(',')|default('[^:]*') )) 49 | 50 | # Limitations: Doesn't seem to set users, only groups. Groups are always appended and not deleted. The aclmod mode does not work like above modes and replace values. 51 | - name: Permission > create/modify acls 52 | shell: > 53 | pveum aclmod {{ item.path }} -roles "{{ item.roles|sort|join(',') }}" 54 | {{ (' -groups "' ~ item.groups|sort|join(',') ~ '"') if item.groups is defined else '' }} 55 | {{ (' -propagate "' ~ item.propagate ~ '"') if item.propagate is defined else '' }} 56 | {{ (' -users "' ~ item.users|sort|join(',') ~ '"') if item.users is defined else '' }} 57 | with_items: "{{ proxmoxy__permission_acls }}" 58 | when: > 59 | item and not __proxmoxy_users.stdout | 60 | regex_search( "acl:{0}:{1}:{2}:{3}:".format( 61 | item.propagate|default(1), item.path|default('/'), 62 | '@?' ~ (item.groups|default([]) + item.users|default([]))|sort|join(',@?')|default('[^:]*'), 63 | item.roles|sort|join(',')|default('[^:]*') )) 64 | 65 | # cat /etc/pve/user.cfg 66 | # role:Sys_Power:Sys.PowerMgmt,Sys.Console: 67 | # acl:1:/:@admins:PVEAuditor,PVEDatastoreUser,PVEVMAdmin,Sys_Power: 68 | -------------------------------------------------------------------------------- /tasks/storage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ## manage/create Storage of type nfs, dir and zfspool 4 | 5 | # Limitation: Always or never sets values for existing storage. (depends on variable) 6 | # pvesm Bug ? currently pvesm does not list disabled storage 7 | 8 | - name: Storage > get storage list before remove 9 | command: pvesm status 10 | register: __proxmoxy_storage_status 11 | changed_when: false 12 | 13 | - name: Storage > remove storages 14 | shell: > 15 | pvesm remove {{ item }} 16 | with_items: "{{ proxmoxy__storage_remove }}" 17 | # not sure if \n in string from above is evaluated 18 | when: > 19 | item and __proxmoxy_storage_status.stdout | regex_search( '[\nn]' ~ item ~ '\s+') 20 | 21 | - name: Storage > get storage list after remove 22 | command: pvesm status 23 | register: __proxmoxy_storage_status 24 | changed_when: false 25 | 26 | - name: Storage > create storage 27 | shell: > 28 | pvesm add {{ item.type }} {{ item.id }} 29 | {#- nfs -#} 30 | {{ ' -server "' ~ item.server ~ '"' if item.server is defined else '' }} 31 | {{ ' -export "' ~ item.export ~ '"' if item.export is defined else '' }} 32 | {{ ' -path "' ~ item.path ~ '"' if item.path is defined else '' }} 33 | {{ ' -options "' ~ item.options ~ '"' if item.options is defined else '' }} 34 | {#- zfs (no maxfiles) -#} 35 | {{ ' -pool "' ~ item.pool ~ '"' if item.pool is defined else '' }} 36 | {{ ' -blocksize "' ~ item.blocksize ~ '"' if item.blocksize|d(None) else '' }} 37 | {{ ' -sparse "' ~ item.sparse|int ~ '"' if item.sparse is defined else '' }} 38 | {#- dir (uses path too) -#} 39 | {{ ' -shared "' ~ item.shared|int ~ '"' if item.shared is defined else '' }} 40 | {#- common -#} 41 | {{ ' -maxfiles "' ~ item.maxfiles ~ '"' if item.maxfiles|d(None) else '' }} 42 | {{ ' -nodes "' ~ item.nodes|d(proxmoxy__storage_nodes)|sort|join(',') ~ '"' if item.nodes|d(proxmoxy__storage_nodes) else '' }} 43 | {{ ' -content "' ~ item.content|d(proxmoxy__storage_content)|sort|join(',') ~ '"' }} 44 | {{ ' -disable "' ~ item.disable|d(0) ~ '"' }} 45 | with_items: "{{ proxmoxy__storage }}" 46 | when: > 47 | not ansible_check_mode and item and not __proxmoxy_storage_status.stdout | 48 | regex_search("{0}\s+{1}\s".format(item.id|default(omit), item.type|default(omit))) 49 | 50 | # how create on already defined would not fail the playbook. 51 | # register: __result 52 | # failed_when: "__result.rc != 0 and 'already defined' not in __result.stderr" 53 | # changed_when: "__result.rc == 0" 54 | 55 | - name: Storage > set storage options if {{ proxmoxy__storage_changes }} 56 | shell: > 57 | pvesm set {{ item.id }} 58 | {#- nfs -#} 59 | {{ ' -options "' ~ item.options ~ '"' if item.options is defined else '' }} 60 | {#- zfs (no maxfiles) -#} 61 | {{ ' -blocksize "' ~ item.blocksize ~ '"' if item.blocksize|d(None) else '' }} 62 | {{ ' -sparse "' ~ item.sparse|int ~ '"' if item.sparse is defined else '' }} 63 | {#- dir (uses path too) -#} 64 | {{ ' -shared "' ~ item.shared|int ~ '"' if item.shared is defined else '' }} 65 | {#- common -#} 66 | {{ ' -maxfiles "' ~ item.maxfiles ~ '"' if item.maxfiles|d(None) else '' }} 67 | {{ ' -nodes "' ~ item.nodes|d(proxmoxy__storage_nodes)|sort|join(',') ~ '"' if item.nodes|d(proxmoxy__storage_nodes) else '' }} 68 | {{ ' -content "' ~ item.content|d(proxmoxy__storage_content)|sort|join(',') ~ '"' }} 69 | {{ ' -disable "' ~ item.disable|d(0) ~ '"' }} 70 | with_items: "{{ proxmoxy__storage }}" 71 | when: > 72 | item and proxmoxy__storage_changes and 73 | __proxmoxy_storage_status.stdout | regex_search( item.id ~ '\s+' ~ item.type ) 74 | 75 | 76 | # Alternative idea from: https://github.com/reminec/ansible-role-proxmox/blob/master/tasks/storage.yml 77 | # only write a pvesm.yml file and include it with a loop 78 | # - name: storage > Setup storages (Only LVM) 79 | # include: pvesm.yml 80 | # with_items: '{{ proxmox_storages }}' 81 | # when: item.type == 'lvm' 82 | 83 | 84 | # Idea for implementation: get config for single config and call set only if changes. 85 | ## TBD set options in local_fact like debops? 86 | # sed for getting 1 storage config: sed -n '/^\w\+: tank$/,/^$/p' storage.cfg | grep "^\W" 87 | # Problem: How to get into fact dict {id: xxx, config: "xxx"}? Would have to workaround with additional command calls? 88 | -------------------------------------------------------------------------------- /library/pvesh.md: -------------------------------------------------------------------------------- 1 | # pvesh example commands 2 | 3 | pvesh get /nodes/moximoz/storage/local-zfs/content 4 | 200 OK 5 | [ 6 | { 7 | "content" : "images", 8 | "format" : "subvol", 9 | "name" : "subvol-100-disk-1", 10 | "parent" : null, 11 | "size" : 4294967296, 12 | "vmid" : "100", 13 | "volid" : "local-zfs:subvol-100-disk-1" 14 | }, 15 | { 16 | "content" : "images", 17 | "format" : "subvol", 18 | "name" : "subvol-201-disk-1", 19 | "parent" : null, 20 | "size" : 6442450944, 21 | "vmid" : "201", 22 | "volid" : "local-zfs:subvol-201-disk-1" 23 | }, 24 | ] 25 | 26 | ## Test API disk create on existing 27 | 28 | zfs create -o refquota=4G rpool/data/subvol-300-man-2 29 | pvesh create /nodes/moximoz/storage/local-zfs/content -format=subvol -filename=subvol-300-man-2 -size=6G -vmid=300 30 | 31 | # delete 32 | pvesh delete /nodes/moximoz/storage/local-zfs/content/subvol-300-man-2 33 | 34 | ## Create new VM on existing disk (subvol must exist) 35 | pvesh create /nodes/moximoz/lxc -vmid=302 -ostemplate=/var/lib/vz/template/cache/centos-7-default_20161207_amd64.tar.xz -storage=local-zfs -rootfs=local-zfs:subvol-302-exists-1,acl=1,size=4G 36 | 37 | ## misc unsorted commands 38 | 39 | pvesh get /cluster/nextid 40 | pvesh get /cluster/resources 41 | 200 OK 42 | [ 43 | { 44 | "cpu" : 0, 45 | "disk" : 0, 46 | "diskread" : 0, 47 | "diskwrite" : 0, 48 | "id" : "lxc/100", 49 | "maxcpu" : 2, 50 | "maxdisk" : 4294967296, 51 | "maxmem" : 1073741824, 52 | "mem" : 0, 53 | "name" : "testimi", 54 | "netin" : 0, 55 | "netout" : 0, 56 | "node" : "moximoz", 57 | "status" : "stopped", 58 | "template" : 0, 59 | "type" : "lxc", 60 | "uptime" : 0, 61 | "vmid" : 100 62 | }, 63 | { 64 | "cpu" : 0, 65 | "disk" : 249036800, 66 | "diskread" : 53248, 67 | "diskwrite" : 0, 68 | "id" : "lxc/203", 69 | "maxcpu" : 2, 70 | "maxdisk" : 4294967296, 71 | "maxmem" : 536870912, 72 | "mem" : 3330048, 73 | "name" : "XY2", 74 | "netin" : 0, 75 | "netout" : 0, 76 | "node" : "moximoz", 77 | "status" : "running", 78 | "template" : 0, 79 | "type" : "lxc", 80 | "uptime" : 9474, 81 | "vmid" : 203 82 | }, 83 | { 84 | "cpu" : 0, 85 | "disk" : 0, 86 | "diskread" : 0, 87 | "diskwrite" : 0, 88 | "id" : "lxc/201", 89 | "maxcpu" : 2, 90 | "maxdisk" : 6442450944, 91 | "maxmem" : 536870912, 92 | "mem" : 0, 93 | "name" : "YYY", 94 | "netin" : 0, 95 | "netout" : 0, 96 | "node" : "moximoz", 97 | "status" : "stopped", 98 | "template" : 0, 99 | "type" : "lxc", 100 | "uptime" : 0, 101 | "vmid" : 201 102 | }, 103 | { 104 | "cpu" : 0.0231350590742604, 105 | "disk" : 1468399616, 106 | "id" : "node/moximoz", 107 | "level" : "", 108 | "maxcpu" : 2, 109 | "maxdisk" : 115041632256, 110 | "maxmem" : 8302227456, 111 | "mem" : 1191059456, 112 | "node" : "moximoz", 113 | "type" : "node", 114 | "uptime" : 10040 115 | }, 116 | { 117 | "disk" : 196608, 118 | "id" : "storage/moximoz/tank_multimedia", 119 | "maxdisk" : 1930587111424, 120 | "node" : "moximoz", 121 | "storage" : "tank_multimedia", 122 | "type" : "storage" 123 | }, 124 | { 125 | "disk" : 1468399616, 126 | "id" : "storage/moximoz/local", 127 | "maxdisk" : 115041632256, 128 | "node" : "moximoz", 129 | "storage" : "local", 130 | "type" : "storage" 131 | }, 132 | { 133 | "disk" : 131072, 134 | "id" : "storage/moximoz/dir_tank_data", 135 | "maxdisk" : 1930587013120, 136 | "node" : "moximoz", 137 | "storage" : "dir_tank_data", 138 | "type" : "storage" 139 | }, 140 | { 141 | "disk" : 747372544, 142 | "id" : "storage/moximoz/local-zfs", 143 | "maxdisk" : 114320650240, 144 | "node" : "moximoz", 145 | "storage" : "local-zfs", 146 | "type" : "storage" 147 | }, 148 | { 149 | "disk" : 884736, 150 | "id" : "storage/moximoz/tank", 151 | "maxdisk" : 1930587799552, 152 | "node" : "moximoz", 153 | "storage" : "tank", 154 | "type" : "storage" 155 | } 156 | ] 157 | 158 | pvesh get /nodes/moximoz/lxc/209/config 159 | 200 OK 160 | { 161 | "arch" : "amd64", 162 | "digest" : "cb9329f7b974fce17885d93598cfcae4ebdc6ddf", 163 | "hostname" : "YYY9", 164 | "memory" : 512, 165 | "onboot" : 0, 166 | "ostype" : "centos", 167 | "rootfs" : "local-zfs:subvol-209-disk-1,size=4G", 168 | "swap" : 0 169 | } 170 | 171 | pvesh set /nodes/moximoz/lxc/209/config -onboot 0 172 | 200 OK 173 | 174 | pvesh set /nodes/moximoz/lxc/209/config -delete cores,swap 175 | 200 OK 176 | 177 | pvesh create /nodes/moximoz/storage/tank_multimedia/content -format=subvol -filename=subvol-300-test1 -vmid=300 -size=4G 178 | 200 OK 179 | 180 | pvesh set /nodes/moximoz/lxc/300/config -cores 1 -cpulimit 0.4 -mp0=volume=tank_multimedia:subvol-300-test0,mp=/mountinvm0 181 | 200 OK 182 | 183 | 184 | -------------------------------------------------------------------------------- /tasks/prov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: provision > find all template files 4 | command: "find -type f -printf '%P\n'" 5 | args: 6 | chdir: "{{ proxmoxy__templates_dir }}" 7 | changed_when: false 8 | register: __proxmoxy_prov_reg_templates 9 | 10 | - name: provision > get list of current VMIDs 11 | shell: > 12 | pct list | cut -f1 -d' ' | grep -iv VMID || echo 'empty' 13 | changed_when: false 14 | register: __proxmoxy_prov_reg_vmids 15 | 16 | # Compute the right ostemplate file for every container. 17 | # ostemplate can contain a (regex) pattern to match the filename, i.e. "centos-7.*20160404.*" 18 | # complicated statement, therefor outsourced to its own set_fact var 19 | - name: provision > populate matching templates for all containers 20 | set_fact: 21 | __proxmoxy_ct_template_match: "{{ __proxmoxy_ct_template_match|default({}) | combine( {item.vmid: __proxmoxy_prov_reg_templates.stdout_lines|select('search', item.ostemplate|default(proxmoxy__templates_default))|sort|last|string} ) }}" 22 | with_items: "{{ proxmoxy__provision_containers }}" 23 | when: not ansible_check_mode 24 | 25 | - name: provision > configure lxc container 26 | proxmox_prov: 27 | state: "{{ item.state|default('present') }}" 28 | vmid: "{{ item.vmid }}" 29 | password: "{{ item.password|default(omit) if item.password is defined else lookup('password', secret|d('secret') + '/credentials/' + 30 | item.hostname|default('lxc') + ('.' + item.searchdomain if 31 | item.searchdomain|d() else '') + '/root/pw length=32') }}" 32 | hostname: "{{ item.hostname|default('lxc') }}" 33 | ostemplate: "{{ proxmoxy__templates_dir }}/{{ __proxmoxy_ct_template_match[item.vmid]|default(omit) }}" 34 | arch: "{{ item.arch|default(omit) }}" 35 | cmode: "{{ item.cmode|default(omit) }}" 36 | console: "{{ item.console|default(omit) }}" 37 | cores: "{{ item.cores|default(omit) }}" 38 | cpuunits: "{{ item.cpuunits|default(omit) }}" 39 | description: "{{ item.description|default(omit) }}" 40 | memory: "{{ item.memory|default(omit) }}" 41 | mp0: "{{ item.mp0|default(omit) }}" 42 | mp1: "{{ item.mp1|default(omit) }}" 43 | mp2: "{{ item.mp2|default(omit) }}" 44 | mp3: "{{ item.mp3|default(omit) }}" 45 | mp4: "{{ item.mp4|default(omit) }}" 46 | mp5: "{{ item.mp5|default(omit) }}" 47 | mp6: "{{ item.mp6|default(omit) }}" 48 | mp7: "{{ item.mp7|default(omit) }}" 49 | mp8: "{{ item.mp8|default(omit) }}" 50 | mp9: "{{ item.mp9|default(omit) }}" 51 | net0: "{{ item.net0 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net0|d({}) and item.net0.bridge is undefined else item.net0|default(omit) }}" 52 | net1: "{{ item.net1 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net1|d({}) and item.net1.bridge is undefined else item.net1|default(omit) }}" 53 | net2: "{{ item.net2 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net2|d({}) and item.net2.bridge is undefined else item.net2|default(omit) }}" 54 | net3: "{{ item.net3 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net3|d({}) and item.net3.bridge is undefined else item.net3|default(omit) }}" 55 | net4: "{{ item.net4 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net4|d({}) and item.net4.bridge is undefined else item.net4|default(omit) }}" 56 | net5: "{{ item.net5 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net5|d({}) and item.net5.bridge is undefined else item.net5|default(omit) }}" 57 | net6: "{{ item.net6 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net6|d({}) and item.net6.bridge is undefined else item.net6|default(omit) }}" 58 | net7: "{{ item.net7 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net7|d({}) and item.net7.bridge is undefined else item.net7|default(omit) }}" 59 | net8: "{{ item.net8 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net8|d({}) and item.net8.bridge is undefined else item.net8|default(omit) }}" 60 | net9: "{{ item.net9 | combine({'bridge': proxmoxy__provision_bridge|d('vmbr0')}) if item.net9|d({}) and item.net9.bridge is undefined else item.net9|default(omit) }}" 61 | node: "{{ item.node|default(omit) }}" 62 | onboot: "{{ item.onboot|default(omit) }}" 63 | ostype: "{{ item.ostype|default(omit) }}" 64 | pool: "{{ item.pool|default(omit) }}" 65 | protection: "{{ item.protection|default(omit) }}" 66 | rootfs: "{{ item.rootfs|default(omit) }}" 67 | searchdomain: "{{ item.searchdomain|default(omit) }}" 68 | ssh_public_keys: "{{ item.ssh_public_keys|default(omit) }}" 69 | startup: "{{ item.startup|default(omit) }}" 70 | storage: "{{ item.storage|default(omit) }}" 71 | swap: "{{ item.swap|default(omit) }}" 72 | tty: "{{ item.tty|default(omit) }}" 73 | unprivileged: "{{ item.unprivileged|default(omit) }}" 74 | with_items: "{{ proxmoxy__provision_containers }}" 75 | register: __proxmoxy_register_containers 76 | 77 | - name: Provision > enable lxc idmapping uid 78 | lineinfile: 79 | dest: "{{ proxmoxy__provision_config_path + '/' + item.vmid|string + '.conf'}}" 80 | regexp: "^lxc.idmap:.u {{ proxmoxy__provision_idmap_from|string + ' ' + proxmoxy__provision_idmap_start|string + ' ' + proxmoxy__provision_idmap_range|string }}" 81 | line: "lxc.idmap: u {{ proxmoxy__provision_idmap_from|string + ' ' + proxmoxy__provision_idmap_start|string + ' ' + proxmoxy__provision_idmap_range|string }}" 82 | create: no 83 | state: present 84 | when: proxmoxy__provision_idmapping 85 | with_items: "{{ proxmoxy__provision_containers }}" 86 | 87 | - name: Provision > enable lxc idmapping gid 88 | lineinfile: 89 | dest: "{{ proxmoxy__provision_config_path + '/' + item.vmid|string + '.conf'}}" 90 | regexp: "^lxc.idmap:.g {{ proxmoxy__provision_idmap_from|string + ' ' + proxmoxy__provision_idmap_start|string + ' ' + proxmoxy__provision_idmap_range|string }}" 91 | line: "lxc.idmap: g {{ proxmoxy__provision_idmap_from|string + ' ' + proxmoxy__provision_idmap_start|string + ' ' + proxmoxy__provision_idmap_range|string }}" 92 | create: no 93 | state: present 94 | when: proxmoxy__provision_idmapping 95 | with_items: "{{ proxmoxy__provision_containers }}" 96 | 97 | # - debug: 98 | # var: item.item.vmid 99 | # verbosity: 1 100 | # when: item.item.vmid|string not in __proxmoxy_prov_reg_vmids.stdout_lines 101 | # with_items: "{{ __proxmoxy_register_containers.results }}" 102 | 103 | # If vm (vmid) was not here before (on top), it must be a new VM. 104 | - name: Provision > start new containers 105 | shell: > 106 | pct start {{ item.item.vmid | d() }} 107 | when: not ansible_check_mode and item and item.item.vmid | string not in __proxmoxy_prov_reg_vmids.stdout_lines and (proxmoxy__provision_post_cmds|length or item.item.onboot|d(False)|bool) 108 | with_items: 109 | - "{{ __proxmoxy_register_containers.results|d([]) }}" 110 | 111 | - name: Provision > execute commands on new containers 112 | shell: > 113 | echo '{{ item[1] }}' | pct enter {{ item[0].item.vmid|d() }} 114 | when: not ansible_check_mode and item[0] and item[0].item.vmid|string not in __proxmoxy_prov_reg_vmids.stdout_lines 115 | with_nested: 116 | - "{{ __proxmoxy_register_containers.results|d([]) }}" 117 | - "{{ proxmoxy__provision_post_cmds|d([]) }}" 118 | ignore_errors: True 119 | 120 | - name: Provision > stop new containers, unless onboot 121 | shell: > 122 | pct stop {{ item.item.vmid|d() }} 123 | when: not ansible_check_mode and item and (item.item.vmid|string not in __proxmoxy_prov_reg_vmids.stdout_lines) and not item.item.onboot|d(False)|bool and proxmoxy__provision_post_cmds|length 124 | with_items: 125 | - "{{ __proxmoxy_register_containers.results|d([]) }}" 126 | 127 | - name: Provision > ensure bootstrap hosts file exists 128 | local_action: lineinfile dest='{{ proxmoxy__provision_bootstrap_file }}' 129 | regexp="^\[\S+\]$" line='[{{ proxmoxy__provision_bootstrap_entry }}]' 130 | state=present 131 | # workaround with length, "hosts/bootstrap" throws error like unknown variable "hosts" 132 | when: proxmoxy__provision_bootstrap_file|d('')|string|length 133 | become: False 134 | 135 | - name: Provision > write container to end of bootstrapping hosts file 136 | local_action: lineinfile dest='{{ proxmoxy__provision_bootstrap_file }}' 137 | regexp="^{{ item.item.hostname|d() + ('.' + item.item.searchdomain if item.item.searchdomain is defined else '') }}.*$" 138 | line='{{ item.item.hostname|d() + ("." + item.item.searchdomain if item.item.searchdomain is defined else "") }} 139 | ansible_host={{ item.item.net0.ip|d('127.0.1.1')|ipaddr("address") if item.item.net0 is defined else '127.0.1.1' }} 140 | ansible_user=root' 141 | state=present 142 | when: not ansible_check_mode and proxmoxy__provision_bootstrap_file|d('')|string|length and item.item.vmid|string not in __proxmoxy_prov_reg_vmids.stdout_lines 143 | with_items: 144 | - "{{ __proxmoxy_register_containers.results|d([]) }}" 145 | loop_control: 146 | label: "{{ item.item.hostname|d('undefined') }}" 147 | become: False 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: proxmoxy 2 | 3 | Configures proxmox (4.x - 8.x) hosts and provisions lxc vm containers. It does this by directly interfacing with the proxmox shell tools on the proxmox host. There is no direct dependency on the proxmox http API or libraries like proxmoxer. 4 | 5 | For container management it ships with the [proxmox_prov](library/proxmox_prov.py) Ansible module. 6 | 7 | Proxmoxy is able to: 8 | * Host: configure Proxmox host system 9 | * Templates: LXC Template management/downloading 10 | * Permission: set Proxmox Users/Groups, Permissions and ACL 11 | * Storage: management of storages *(type dir|zfspool|nfs)* 12 | * Provision: Create and configure LXC Containers with the proxmox_prov Module 13 | 14 | ## Requirements 15 | 16 | None. 17 | 18 | ## Dependencies 19 | 20 | None. 21 | 22 | ## Installation 23 | 24 | ### Ansible 2+ 25 | 26 | Using ansible galaxy: 27 | 28 | ```bash 29 | ansible-galaxy install mozitat.proxmoxy 30 | ``` 31 | 32 | ## Role Variables 33 | 34 | All variables are defined in [`defaults/main.yml`](defaults/main.yml). See the Examples section for possible values. 35 | 36 | ### Host 37 | 38 | | Name | Default | Type | Description | 39 | | ------------------------------------- | -------------------------- | ------- | --------------------------------------- | 40 | | `proxmoxy__host_modules` | `{}` | dict | Load extra kernel modules on boot | 41 | | `proxmoxy__host_tuntap` | `true` | Boolean | Enable tun/tap for lxc ct | 42 | | `proxmoxy__host_remove_nosubnag` | `true` | Boolean | Remove no-subscription message on login | 43 | | `proxmoxy__host_repo_enterprise` | `false` | Boolean | Disable/enable enterprise repo | 44 | | `proxmoxy__host_repo_nosubs` | `true` | Boolean | Enable no-subscription repo | 45 | 46 | ### Templates 47 | 48 | *For a list of all templates: `pveam available`* 49 | 50 | | Name | Default | Type | Description | 51 | | ------------------------------------- | ------------------------------ | ------- | ----------------------------------------------------------- | 52 | | `proxmoxy__templates_storage` | `"local"` | string | Which storage to use for template storage | 53 | | `proxmoxy__templates_dir` | `"/var/lib/vz/template/cache"` | string | The template files directory | 54 | | `proxmoxy__templates_default` | `"centos-7"` | string | Default template used in provisioning | 55 | | `proxmoxy__templates_update` | `false` | Boolean | Always update template list with `pveam update`? | 56 | | `proxmoxy__templates` | `[]` | list | Download these templates, can use python regex expressions. | 57 | 58 | ### Permission 59 | 60 | *This module uses the pveum utility. `man pveum`* 61 | 62 | | Name | Default | Type | Description | 63 | | ------------------------------------- | -------------------------- | ------------- | ------------------------------ | 64 | | `proxmoxy__permission_groups` | `[{}]` | list of dicts | Create these groups | 65 | | `proxmoxy__permission_users` | `[{}]` | list of dicts | Create users, the uid "user@pam" has to exist on the host | 66 | | `proxmoxy__permission_roles` | `[{}]` | list of dicts | Create roles | 67 | | `proxmoxy__permission_acls` | `[{}]` | list of dicts | ACL values will be appended to the current value | 68 | 69 | ### Storage 70 | 71 | *Currently cannot detect changes in single storage items, you can choose between set only on creation or always set.* 72 | 73 | | Name | Default | Type | Description | 74 | | ------------------------------------- | -------------------------- | ------------- | ------------------------------ | 75 | | `proxmoxy__storage` | `[{}]` | list of dicts | PVE storages to create | 76 | | `proxmoxy__storage_content` | `['images', 'rootdir']` | list | Default storage content | 77 | | `proxmoxy__storage_nodes` | `[]` | list | Default list of nodes for storages, leave empty for all. | 78 | | `proxmoxy__storage_changes` | `true` | Boolean | Always set storage settings. | 79 | | `proxmoxy__storage_remove` | `[]` | list | List of storage items to remove. Items in this list normally should not appear in `proxmoxy__storage` | 80 | 81 | ### Provision 82 | 83 | | Name | Default | Type | Description | 84 | | ------------------------------------- | -------------------------- | ------- | ------------------------------ | 85 | | `proxmoxy__provision_secret` | `true` | Boolean | Read/save credentials from secret folder, [debops.secret](https://github.com/debops/ansible-secret) compatible | 86 | | `proxmoxy__provision_bridge` | `"vmbr0"` | string | Default bridge definition for netX networks | 87 | | `proxmoxy__provision_containers` | `[]` | list | List of container configurations | 88 | | `proxmoxy__provision_post_cmds` | `[]` | list | List of simple commands to run in new containers, may use quotes, but no pipes or redirection. | 89 | | `proxmoxy__provision_bootstrap_file` | `'hosts/bootstrap'` | string | Write inventory entries for new containers into this file, disable by setting to "" | 90 | | `proxmoxy__provision_bootstrap_entry` | `'bootstrap_hosts'` | string | The inventory section name in `proxmoxy__provision_bootstrap_file` | 91 | 92 | 93 | ## Example Playbook 94 | 95 | 96 | ```yaml 97 | - hosts: all 98 | vars: 99 | proxmoxy__host_modules: 100 | lm-sensors: w83627ehf 101 | 102 | proxmoxy__templates: 103 | - 'centos-[67]{1}-.*' 104 | - 'debian-8..-standard' 105 | - 'ubuntu-16.[0-9]+-standard'] 106 | 107 | proxmoxy__permission_groups: 108 | - name: admins 109 | comment: 'Admins Group' 110 | - name: group1 111 | comment: 'another group' 112 | 113 | proxmoxy__permission_users: 114 | - name: myuser@pam 115 | comment: 'Dis my user' 116 | email: 'myuser@bla.at' 117 | enable: True, 118 | expire: 0 119 | firstname: 'Max' 120 | groups: 'admins,group1' 121 | key: '' 122 | lastname: 'Muster' 123 | password: Null 124 | 125 | proxmoxy__permission_roles: 126 | - name: Sys_Power 127 | privs: 128 | - 'Sys.PowerMgmt' 129 | - 'Sys.Console' 130 | 131 | proxmoxy__permission_acls: 132 | - path: '/' 133 | roles: 134 | - 'PVEAuditor' 135 | - 'PVEDatastoreUser' 136 | - 'PVEVMAdmin' 137 | - 'Sys_Power' 138 | propagate: 1 139 | groups: 140 | - 'group1' 141 | 142 | proxmoxy__storage: 143 | - type: zfspool # mandatory 'type, id, pool' 144 | id: storage-zfs 145 | pool: tank/data 146 | blocksize: Null 147 | sparse: 1 148 | content: ['images', 'rootdir'] 149 | disable: 0 150 | nodes: ['mynode1'] 151 | - type: dir # mandatory 'type id path' 152 | id: storage-dir 153 | path: /tank/somedir 154 | maxfiles: 3 155 | shared: 0 156 | - type: nfs # mandatory 'type id server export path' 157 | id: storage-nfs 158 | server: 192.168.1.2 159 | export: /myexport 160 | path: /mnt/nfs_myexport 161 | options: 'vers=3,soft' 162 | maxfiles: 7 163 | content: ['images', 'rootdir', 'vztmpl', 'backup', 'iso'] 164 | 165 | proxmoxy__storage_remove: 166 | - 'tank-remove' 167 | - 'andremoveme' 168 | 169 | proxmoxy__provision_containers: 170 | - vmid: 210 171 | state: present 172 | ostemplate: "centos-7.*" 173 | password: abc1234 # ignored if secret is used 174 | storage: local-zfs 175 | hostname: ct210 176 | memory: 512 177 | onboot: True 178 | net0: 179 | name: eth0 180 | bridge: vmbr0 181 | ip: 192.168.1.210/24 182 | gw: 192.168.1.1 183 | 184 | roles: 185 | - role: mozitat.proxmoxy 186 | ``` 187 | 188 | For a more complete container definition with all options see [`examples/containers.yml`](examples/containers.yml). 189 | 190 | ## License 191 | 192 | MIT 193 | 194 | ## Author Information 195 | 196 | mozit.eu 197 | 198 | 199 | -------------------------------------------------------------------------------- /library/test_proxmox_prov.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from . import proxmox_prov as pv 3 | from .conftest import CHANGE_DATA, mergedict, DIB, DCH, DEXP 4 | import pytest 5 | 6 | # run with watch 7 | # ptw -c ./library/ 8 | # sound: 9 | # ptw -c --onfail "echo -e '\a'" ./library/ 10 | # ptw -c --onfail "paplay /usr/share/sounds/freedesktop/stereo/suspend-error.oga" ./library/ 11 | 12 | Adict = {'state': 'present', 'vmid': 333, 'hostname': 'myhost', 'node': 'A', 13 | 'onboot': False, 'ostemplate': None, 'delete': ['A', 'B']} 14 | 15 | 16 | # This is much shorter! 17 | @pytest.mark.parametrize('input', [ 18 | ({'cores': 2}), 19 | ({'cores': 2, 'cpulimit': '0.5'}), 20 | ({'cores': 2, 'cpulimit': '0.5', 'delete': ['cores', 'cpulimit']}), 21 | # pytest.mark.xfail(({'cores': 99})) 22 | ]) 23 | def test_get_vm_changes(input): 24 | vm = pv.get_vm_by_id(201, pv.get_cluster_resources()['data']) 25 | result = pv.get_vm_config(vm) 26 | cfg = result['data'] 27 | data = CHANGE_DATA['201 basic'].copy() 28 | data.update(input) 29 | changes = pv.get_vm_changes(data, cfg, True) 30 | assert changes == input 31 | 32 | 33 | # @pytest.mark.skip() 34 | @pytest.mark.parametrize('input, expect', [ 35 | (DIB[0].copy(), {}), 36 | (mergedict(DIB[0], DCH[0]), DEXP[0]), 37 | (mergedict(DIB[0], DCH[1]), DEXP[1]), 38 | (mergedict(DIB[0], DCH[2]), DEXP[2]), 39 | ]) 40 | def test_get_vm_changes_disk(input, expect): 41 | vm = pv.get_vm_by_id(201, pv.get_cluster_resources()['data']) 42 | cfg = pv.get_vm_config(vm)['data'] 43 | changes = pv.get_vm_changes(input, cfg, True) 44 | assert changes == expect 45 | 46 | 47 | @pytest.mark.parametrize('input, expect', [ 48 | # dict to command line 49 | ({'storage': 'tank_multimedia', 'volume': 'subvol-201-disk-1', 50 | 'mp': '/multimedia', 'acl': True, 'size': '8G'}, 51 | 'tank_multimedia:subvol-201-disk-1,mp=/multimedia,acl=1,size=8G'), 52 | # command line to dict 53 | ('tank_multimedia:subvol-201-disk-1,mp=/multimedia,acl=0,size=8G', 54 | {'storage': 'tank_multimedia', 'volume': 'subvol-201-disk-1', 55 | 'mp': '/multimedia', 'acl': False, 'size': '8G'}), 56 | # dict to command line with dir mount and reverse 57 | ({'volume': '/mnt/dirmount', 58 | 'mp': '/dir_mount', 'size': '0G'}, 59 | '/mnt/dirmount,mp=/dir_mount,size=0G'), 60 | ('/mnt/dirmount,mp=/dir_mount,size=0G', 61 | {'volume': '/mnt/dirmount', 62 | 'mp': '/dir_mount', 'size': '0G'}), 63 | # simulate api returning 0T for 0 size 64 | ('/mnt/dirmount,mp=/dir_mount,size=0T', 65 | {'volume': '/mnt/dirmount', 66 | 'mp': '/dir_mount', 'size': '0T'}), 67 | # netX both ways 68 | ({'name': 'eth0', 'bridge': 'vmbr0', 'hwaddr': 'as:04:23:ds:32', 'ip': '192.168.2.1/24',}, 69 | 'name=eth0,bridge=vmbr0,hwaddr=as:04:23:ds:32,ip=192.168.2.1/24'), 70 | ('name=eth0,bridge=vmbr0,firewall=0', {'name': 'eth0', 'bridge': 'vmbr0', 'firewall': False},), 71 | ('name=eth1,bridge=vmbr0,gw=192.168.178.1,hwaddr=B6:75:39:CC:46:B1,' 72 | 'ip=192.168.178.207/24,mtu=1400,tag=3,type=veth', 73 | {'name': 'eth1', 'bridge': 'vmbr0', 'gw': '192.168.178.1', 'hwaddr': 74 | 'B6:75:39:CC:46:B1', 'ip': '192.168.178.207/24', 'mtu': '1400', 'tag': '3', 75 | 'type': 'veth'}), 76 | # (, ,), 77 | ]) 78 | def test_convert_params(input, expect): 79 | result = pv.convert_params(input) 80 | assert result == expect 81 | 82 | 83 | @pytest.mark.parametrize('input, pve_string, expect', [ 84 | ({'acl': False, 'size': '10G'}, 85 | 'tank_multimedia:subvol-201-disk-1,mp=/multimedia,acl=1,size=8G', 86 | {'size': '10G', 'acl': False}), 87 | ({'storage': 'tank_multimedia', 'volume': 'subvol-201-disk-1', 'mp': '/change', 'acl': True, 'size': '10G'}, 88 | 'tank_multimedia:subvol-201-disk-1,mp=/multimedia,acl=1,size=8G', 89 | {'size': '10G', 'mp': '/change'}), 90 | # (, ,), 91 | ]) 92 | def test_dict_diff(input, pve_string, expect): 93 | changes = pv.get_dict_diff(input, pv.convert_params(pve_string)) 94 | assert changes == expect 95 | 96 | 97 | @pytest.mark.parametrize('inp, exp', [ 98 | ({'volume': 'local-zfs:subvol-101-disk-1', 'size': '4G'}, 99 | ('local-zfs', 'subvol', 'subvol-101-disk-1', '4G')), 100 | ({'storage': 'local-zfs', 'volume': 'subvol-101-disk-1', 'size': '4G'}, 101 | ('local-zfs', 'subvol', 'subvol-101-disk-1', '4G')), 102 | ({'volume': 'local-zfs:subvol-200-diskX'}, 103 | ('local-zfs', 'subvol', 'subvol-200-diskX', '4G')), 104 | ({'volume': 'local:subvol-200-diskX', 'format': 'raw'}, 105 | ('local', 'raw', 'subvol-200-diskX', '4G')), 106 | ({'storage': 'local-zfs', 'size': '6G'}, 107 | ('local-zfs', None, None, '6G')), 108 | # case user uses 0 for size 0G 109 | ({'volume': '/tank/directory', 'size': '0'}, 110 | (None, None, '/tank/directory', '0G')), 111 | # ('bla', 'ba'), 112 | ]) 113 | def test_get_volume_params(inp, exp): 114 | (sto, fmt, volname, sz) = pv.get_volume_params(inp) 115 | assert (sto, fmt, volname, sz) == exp 116 | 117 | 118 | def test_get_vm_config(): 119 | vm = pv.get_vm_by_id(201, pv.get_cluster_resources()['data']) 120 | result = pv.get_vm_config(vm) 121 | assert result['status_code'] == 200 122 | cfg = result['data'] 123 | assert cfg['arch'] == 'amd64' 124 | assert cfg['hostname'] == 'VM201' 125 | 126 | 127 | def test_get_vm_by_id_simple(): 128 | vm = pv.get_vm_by_id(100, pv.get_cluster_resources()['data']) 129 | # this is still vm data from the resource list 130 | assert vm['vmid'] == 100 131 | assert vm['name'] == 'testimi' 132 | 133 | 134 | def test_get_vm_by_id_onlyid(): 135 | vm = pv.get_vm_by_id(201) 136 | assert vm['vmid'] == 201 137 | assert vm['name'] == 'VM201' 138 | assert vm['id'] == 'lxc/201' 139 | 140 | 141 | def test_get_vm_by_hostname_only(): 142 | vm2 = pv.get_vm_by_hostname('NotUnderThisName') 143 | assert vm2 is None 144 | vm = pv.get_vm_by_hostname('testimi') 145 | assert vm['vmid'] == 100 146 | assert vm['name'] == 'testimi' 147 | assert vm['id'] == 'lxc/100' 148 | 149 | 150 | @pytest.mark.parametrize('inp, exp', [ 151 | (100, 'stopped'), 152 | # expects int 153 | pytest.mark.xfail(('testimi', 'stopped')), 154 | ((100, 'moximoz'), 'stopped'), 155 | (999, None), 156 | (None, None), 157 | # ('VMdoesnotexist', None), 158 | ((999, 'moximoz'), None), 159 | ]) 160 | def test_get_vm_status(inp, exp): 161 | try: 162 | st = pv.get_vm_status(*inp) 163 | except TypeError: 164 | st = pv.get_vm_status(inp) 165 | assert st == exp 166 | 167 | 168 | @pytest.mark.parametrize('inp, exp', [ 169 | ((100, 'start'), ['UPID:moximoz', 'vzstart']), 170 | ((100, 'stop'), ['UPID:moximoz', 'vzstop']), 171 | ((999, 'stop'), ['vm does not exist']), 172 | ]) 173 | def test_set_vm_status(inp, exp): 174 | try: 175 | st = pv.set_vm_status(*inp) 176 | except TypeError: 177 | st = pv.set_vm_status(inp) 178 | for e in exp: 179 | if 'does not exist' not in e: 180 | assert st.get('status_code') == 200 181 | assert e in st.get('data') 182 | 183 | 184 | @pytest.mark.parametrize('inp, exp', [ 185 | # doesn't exist 186 | ({'vmid': 999}, (True, False, {'status': 'FAIL'})), 187 | # is stopped 188 | ({'vmid': 100}, (False, True, {'status': 'OK'})), 189 | ({'hostname': 'testimi'}, (False, True, {'status': 'OK'})), 190 | # is running 191 | ({'vmid': 203}, (False, False, {'status': 'OK'})), 192 | # is suspended 193 | ({'vmid': 204}, (False, True, {'status': 'OK'})), 194 | ]) 195 | def test_pve_ct_start(inp, exp): 196 | e, ch, meta = pv.pve_ct_start(inp) 197 | assert e == exp[0] 198 | assert ch == exp[1] 199 | assert list(exp[2].items())[0] in list(meta.items()) 200 | 201 | 202 | @pytest.mark.parametrize('inp, exp', [ 203 | # doesn't exist 204 | ({'vmid': 999}, (True, False, {'status': 'FAIL'})), 205 | # is stopped 206 | ({'vmid': 100}, (False, False, {'status': 'OK'})), 207 | ({'hostname': 'testimi'}, (False, False, {'status': 'OK'})), 208 | # is running 209 | ({'vmid': 203}, (False, True, {'status': 'OK'})), 210 | # TBD status suspended, but suspending does not work here. 211 | # ({'vmid': 204}, (False, True, {'status': 'OK'})), 212 | ]) 213 | def test_pve_ct_stop(inp, exp): 214 | e, ch, meta = pv.pve_ct_stop(inp) 215 | assert e == exp[0] 216 | assert ch == exp[1] 217 | assert list(exp[2].items())[0] in list(meta.items()) 218 | 219 | 220 | @pytest.mark.parametrize('inp, exp', [ 221 | # doesn't exist 222 | ({'vmid': 999}, (True, False, {'status': 'FAIL'})), 223 | # is stopped 224 | ({'vmid': 100}, (False, False, {'status': 'OK'})), 225 | ({'hostname': 'testimi'}, (False, False, {'status': 'OK'})), 226 | # is running 227 | ({'vmid': 203}, (False, True, {'status': 'OK'})), 228 | ({'vmid': 203, 'timeout': 40}, (False, True, {'status': 'OK'})), 229 | ]) 230 | def test_pve_ct_shutdown(inp, exp): 231 | e, ch, meta = pv.pve_ct_shutdown(inp) 232 | assert e == exp[0] 233 | assert ch == exp[1] 234 | assert list(exp[2].items())[0] in list(meta.items()) 235 | 236 | 237 | @pytest.mark.parametrize('inp, exp', [ 238 | # if vmid it just returns it. 239 | ({'vmid': 100}, 100), 240 | ({'vmid': 999}, 999), 241 | ({'vmid': 100, 'hostname': 'testimi'}, 100), 242 | ({'hostname': 'testimi'}, 100), 243 | ]) 244 | def test_get_vm_id(inp, exp): 245 | vmid = pv.get_vm_id(inp) 246 | assert vmid == exp 247 | 248 | 249 | def test_get_cluster_resources(): 250 | res = pv.get_cluster_resources() 251 | assert res['status_code'] == 200 252 | assert res['data'][0]['type'] == 'lxc' 253 | 254 | 255 | def test_get_api_dict_simple(): 256 | new = pv.get_api_dict(Adict) 257 | assert Adict != new 258 | assert new == {'hostname': 'myhost', 'vmid': 333, 'onboot': False, 259 | 'delete': ['A', 'B']} 260 | 261 | 262 | def test_get_api_dict_remove_Nones(): 263 | new = pv.get_api_dict(Adict) 264 | assert Adict != new 265 | assert 'ostemplate' not in new 266 | 267 | 268 | def test_get_api_dict_keep_Falsy(): 269 | new = pv.get_api_dict(Adict) 270 | assert Adict != new 271 | assert new['hostname'] == 'myhost' 272 | assert new['onboot'] is False 273 | 274 | 275 | def test_get_api_format_simple(): 276 | # new = pv.get_api_dict(Adict) 277 | new = pv.get_api_dict({'hostname': 'my', 'onboot': False}) 278 | # line = pv.get_api_format(new, pv.API_PARAM_CONV) 279 | line = pv.get_api_format(new) 280 | assert line == "-hostname=my -onboot=0" 281 | 282 | 283 | def test_get_api_format_with_delete(): 284 | new = pv.get_api_dict({'hostname': 'my', 'onboot': False, 285 | 'delete': ['cores', 'cpulimit']}) 286 | assert 'onboot' in new 287 | line = pv.get_api_format(new, pv.API_PARAM_CONV) 288 | assert line == "-hostname=my -onboot=0 -delete=cores,cpulimit" 289 | -------------------------------------------------------------------------------- /library/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import subprocess 3 | 4 | API_GET = { 5 | '/cluster/resources': '''[ 6 | { 7 | "cpu" : 0, 8 | "disk" : 0, 9 | "diskread" : 0, 10 | "diskwrite" : 0, 11 | "id" : "lxc/100", 12 | "maxcpu" : 2, 13 | "maxdisk" : 4294967296, 14 | "maxmem" : 1073741824, 15 | "mem" : 0, 16 | "name" : "testimi", 17 | "netin" : 0, 18 | "netout" : 0, 19 | "node" : "moximoz", 20 | "status" : "stopped", 21 | "template" : 0, 22 | "type" : "lxc", 23 | "uptime" : 0, 24 | "vmid" : 100 25 | }, 26 | { 27 | "cpu" : 0, 28 | "disk" : 0, 29 | "diskread" : 0, 30 | "diskwrite" : 0, 31 | "id" : "lxc/201", 32 | "maxcpu" : 2, 33 | "maxdisk" : 6442450944, 34 | "maxmem" : 536870912, 35 | "mem" : 0, 36 | "name" : "VM201", 37 | "netin" : 0, 38 | "netout" : 0, 39 | "node" : "moximoz", 40 | "status" : "stopped", 41 | "template" : 0, 42 | "type" : "lxc", 43 | "uptime" : 0, 44 | "vmid" : 201 45 | }, 46 | { 47 | "cpu" : 0, 48 | "disk" : 249036800, 49 | "diskread" : 53248, 50 | "diskwrite" : 0, 51 | "id" : "lxc/203", 52 | "maxcpu" : 2, 53 | "maxdisk" : 4294967296, 54 | "maxmem" : 536870912, 55 | "mem" : 3330048, 56 | "name" : "VM203", 57 | "netin" : 0, 58 | "netout" : 0, 59 | "node" : "moximoz", 60 | "status" : "running", 61 | "template" : 0, 62 | "type" : "lxc", 63 | "uptime" : 9474, 64 | "vmid" : 203 65 | }, 66 | { 67 | "cpu" : 0, 68 | "disk" : 249036800, 69 | "diskread" : 53248, 70 | "diskwrite" : 0, 71 | "id" : "lxc/204", 72 | "maxcpu" : 2, 73 | "maxdisk" : 4294967296, 74 | "maxmem" : 536870912, 75 | "mem" : 3330048, 76 | "name" : "VM204", 77 | "netin" : 0, 78 | "netout" : 0, 79 | "node" : "moximoz", 80 | "status" : "running", 81 | "template" : 0, 82 | "type" : "lxc", 83 | "uptime" : 9474, 84 | "vmid" : 204 85 | }, 86 | { 87 | "cpu" : 0.0231350590742604, 88 | "disk" : 1468399616, 89 | "id" : "node/moximoz", 90 | "level" : "", 91 | "maxcpu" : 2, 92 | "maxdisk" : 115041632256, 93 | "maxmem" : 8302227456, 94 | "mem" : 1191059456, 95 | "node" : "moximoz", 96 | "type" : "node", 97 | "uptime" : 10040 98 | }, 99 | { 100 | "disk" : 196608, 101 | "id" : "storage/moximoz/tank_multimedia", 102 | "maxdisk" : 1930587111424, 103 | "node" : "moximoz", 104 | "storage" : "tank_multimedia", 105 | "type" : "storage" 106 | }, 107 | { 108 | "disk" : 1468399616, 109 | "id" : "storage/moximoz/local", 110 | "maxdisk" : 115041632256, 111 | "node" : "moximoz", 112 | "storage" : "local", 113 | "type" : "storage" 114 | }, 115 | { 116 | "disk" : 131072, 117 | "id" : "storage/moximoz/dir_tank_data", 118 | "maxdisk" : 1930587013120, 119 | "node" : "moximoz", 120 | "storage" : "dir_tank_data", 121 | "type" : "storage" 122 | }, 123 | { 124 | "disk" : 747372544, 125 | "id" : "storage/moximoz/local-zfs", 126 | "maxdisk" : 114320650240, 127 | "node" : "moximoz", 128 | "storage" : "local-zfs", 129 | "type" : "storage" 130 | }, 131 | { 132 | "disk" : 884736, 133 | "id" : "storage/moximoz/tank", 134 | "maxdisk" : 1930587799552, 135 | "node" : "moximoz", 136 | "storage" : "tank", 137 | "type" : "storage" 138 | } 139 | ]''', 140 | '/nodes/moximoz/lxc/100/config': 141 | ''' 142 | { 143 | "arch" : "amd64", 144 | "description" : "My first LXC test container\n", 145 | "digest" : "fe5dcb51e3a024eff80b20916ec75eb68dc9f908", 146 | "hostname" : "testimi", 147 | "memory" : 1024, 148 | "ostype" : "centos", 149 | "rootfs" : "local-zfs:subvol-100-disk-1,size=4G", 150 | "swap" : 512 151 | } 152 | ''', 153 | '/nodes/moximoz/lxc/201/config': 154 | ''' 155 | { 156 | "arch" : "amd64", 157 | "cores" : 1, 158 | "cpulimit" : "0.4", 159 | "digest" : "917c04cd2fb261a1e0d176249e6de9c6bf307d6e", 160 | "hostname" : "VM201", 161 | "memory" : 512, 162 | "mp0" : "tank_multimedia:subvol-201-disk-1,mp=/multimedia,acl=1,size=8G", 163 | "net0" : "name=eth0,bridge=vmbr0,gw=192.168.178.1,hwaddr=B6:75:39:CC:46:B1,ip=192.168.178.232/24,type=veth", 164 | "ostype" : "centos", 165 | "rootfs" : "local-zfs:subvol-201-disk-1,size=6G", 166 | "swap" : 512 167 | } 168 | ''', 169 | '/nodes/moximoz/lxc/203/config': 170 | ''' 171 | { 172 | "arch" : "amd64", 173 | "digest" : "f4c17968e26eb5cde6d3a6e955d5efbb2120d02e", 174 | "hostname" : "VM203", 175 | "memory" : 512, 176 | "ostype" : "centos", 177 | "rootfs" : "local-zfs:subvol-203-disk-1,size=4G", 178 | "swap" : 512 179 | } 180 | ''', 181 | '/nodes/moximoz/storage/local-zfs/content': 182 | ''' 183 | [ 184 | { 185 | "content" : "images", 186 | "format" : "subvol", 187 | "name" : "subvol-100-disk-1", 188 | "parent" : null, 189 | "size" : 4294967296, 190 | "vmid" : "100", 191 | "volid" : "local-zfs:subvol-100-disk-1" 192 | }, 193 | { 194 | "content" : "images", 195 | "format" : "subvol", 196 | "name" : "subvol-201-disk-1", 197 | "parent" : null, 198 | "size" : 6442450944, 199 | "vmid" : "201", 200 | "volid" : "local-zfs:subvol-201-disk-1" 201 | }, 202 | { 203 | "content" : "images", 204 | "format" : "subvol", 205 | "name" : "subvol-203-disk-1", 206 | "parent" : null, 207 | "size" : 4294967296, 208 | "vmid" : "203", 209 | "volid" : "local-zfs:subvol-203-disk-1" 210 | }, 211 | { 212 | "content" : "images", 213 | "format" : "subvol", 214 | "name" : "subvol-209-disk-1", 215 | "parent" : null, 216 | "size" : 4294967296, 217 | "vmid" : "209", 218 | "volid" : "local-zfs:subvol-209-disk-1" 219 | }, 220 | { 221 | "content" : "images", 222 | "format" : "subvol", 223 | "name" : "subvol-211-disk-1", 224 | "parent" : null, 225 | "size" : 4294967296, 226 | "vmid" : "211", 227 | "volid" : "local-zfs:subvol-211-disk-1" 228 | }, 229 | { 230 | "content" : "images", 231 | "format" : "subvol", 232 | "name" : "subvol-300-disk-1", 233 | "parent" : null, 234 | "size" : 4294967296, 235 | "vmid" : "300", 236 | "volid" : "local-zfs:subvol-300-disk-1" 237 | }, 238 | { 239 | "content" : "images", 240 | "format" : "subvol", 241 | "name" : "subvol-300-man-2", 242 | "parent" : null, 243 | "size" : 4294967296, 244 | "vmid" : "300", 245 | "volid" : "local-zfs:subvol-300-man-2" 246 | } 247 | ] 248 | ''', 249 | '/nodes/moximoz/lxc/100/status/current': 250 | ''' 251 | { 252 | "cpu" : 0, 253 | "cpus" : 2, 254 | "disk" : 0, 255 | "diskread" : 0, 256 | "diskwrite" : 0, 257 | "ha" : { 258 | "managed" : 0 259 | }, 260 | "lock" : "", 261 | "maxdisk" : 4294967296, 262 | "maxmem" : 1073741824, 263 | "maxswap" : 536870912, 264 | "mem" : 0, 265 | "name" : "testimi", 266 | "netin" : 0, 267 | "netout" : 0, 268 | "status" : "stopped", 269 | "swap" : 0, 270 | "template" : "", 271 | "type" : "lxc", 272 | "uptime" : 0 273 | } 274 | ''', 275 | '/nodes/moximoz/lxc/203/status/current': 276 | ''' 277 | { 278 | "cpu" : 0, 279 | "disk" : 249036800, 280 | "diskread" : 53248, 281 | "diskwrite" : 0, 282 | "maxcpu" : 2, 283 | "maxdisk" : 4294967296, 284 | "maxmem" : 536870912, 285 | "mem" : 3330048, 286 | "name" : "VM203", 287 | "netin" : 0, 288 | "netout" : 0, 289 | "node" : "moximoz", 290 | "status" : "running", 291 | "template" : "", 292 | "type" : "lxc", 293 | "uptime" : 9474 294 | } 295 | ''', 296 | '/nodes/moximoz/lxc/204/status/current': 297 | ''' 298 | { 299 | "cpu" : 0, 300 | "cpus" : 2, 301 | "disk" : 0, 302 | "diskread" : 0, 303 | "diskwrite" : 0, 304 | "ha" : { 305 | "managed" : 0 306 | }, 307 | "lock" : "", 308 | "maxdisk" : 4294967296, 309 | "maxmem" : 1073741824, 310 | "maxswap" : 536870912, 311 | "mem" : 0, 312 | "name" : "VM204", 313 | "netin" : 0, 314 | "netout" : 0, 315 | "status" : "suspended", 316 | "swap" : 0, 317 | "template" : "", 318 | "type" : "lxc", 319 | "uptime" : 0 320 | } 321 | ''', 322 | '/nodes/moximoz/lxc/999/status/current': 323 | {'error': True, 'rc': 2, 324 | 'output': 'Configuration file \'nodes/moximoz/lxc/1001.conf\' does not exist'}, 325 | } 326 | 327 | API_SET = {} 328 | API_CREATE = { 329 | '/nodes/moximoz/lxc/100/status/start': 330 | ''' 331 | UPID:moximoz:00000EA7:00003052:58AACFA3:vzstart:100:root@pam: 332 | ''', 333 | '/nodes/moximoz/lxc/100/status/stop': 334 | ''' 335 | UPID:moximoz:0000164D:0000ED66:58AAD187:vzstop:100:root@pam: 336 | ''', 337 | '/nodes/moximoz/lxc/100/status/shutdown': 338 | ''' 339 | UPID:moximoz:00002D4D:0014366D:58AEF68B:vzshutdown:100:root@pam: 340 | ''', 341 | '/nodes/moximoz/lxc/203/status/start': 342 | ''' 343 | UPID:moximoz:00000EA7:00003052:58AACFA3:vzstart:203:root@pam: 344 | ''', 345 | '/nodes/moximoz/lxc/203/status/stop': 346 | ''' 347 | UPID:moximoz:00000EA7:00003052:58AACFA3:vzstop:203:root@pam: 348 | ''', 349 | '/nodes/moximoz/lxc/203/status/shutdown': 350 | ''' 351 | UPID:moximoz:00002D4D:0014366D:58AEF68B:vzshutdown:203:root@pam: 352 | ''', 353 | '/nodes/moximoz/lxc/204/status/start': 354 | ''' 355 | UPID:moximoz:00000EA7:00003052:58AACFA3:vzstart:204:root@pam: 356 | ''', 357 | '/nodes/moximoz/lxc/204/status/stop': 358 | ''' 359 | UPID:moximoz:00000EA7:00003052:58AACFA3:vzstop:204:root@pam: 360 | ''', 361 | '/nodes/moximoz/lxc/204/status/shutdown': 362 | ''' 363 | UPID:moximoz:00002D4D:0014366D:58AEF68B:vzshutdown:204:root@pam: 364 | ''', 365 | } 366 | API_DELETE = {} 367 | 368 | # # # Test data for disk/net difference tests, List indices approach. 369 | # Everything will be matched against config from API get /nodes/moximoz/lxc/201/config. 370 | # DIffBase Test data basis 371 | DIB = [ 372 | { 373 | 'state': 'present', 374 | 'vmid': 201, 375 | 'hostname': 'VM201', 376 | 'memory': 512, 377 | 'cores': 1, 378 | 'cpulimit': '0.4', 379 | }, 380 | ] 381 | 382 | # DiffCHanges, What will be changed 383 | DCH = [ 384 | { 385 | 'rootfs': {'storage': 'local-zfs', 386 | 'volume': 'subvol-201-disk-1', 'size': '7G'}, 387 | }, 388 | { 389 | 'rootfs': {'size': '9G'}, 390 | }, 391 | { 392 | 'mp0': { 393 | 'storage': 'tank_multimedia', 394 | 'volume': 'subvol-201-disk-1', 395 | 'mp': '/multimedia', 396 | 'acl': True, 397 | 'shared': True, 398 | 'size': '10G', 399 | }, 400 | }, 401 | ] 402 | 403 | # DiffEXPect Expect Data 404 | DEXP = [ 405 | { 406 | 'rootfs': {'storage': 'local-zfs', 407 | 'volume': 'subvol-201-disk-1', 'size': '7G'}, 408 | '__disk_resize': {'rootfs': {'node': 'moximoz', 409 | 'size': '7G', 410 | 'vmid': 201} 411 | }, 412 | }, 413 | { 414 | 'rootfs': {'storage': 'local-zfs', 415 | 'volume': 'subvol-201-disk-1', 'size': '9G'}, 416 | '__disk_resize': {'rootfs': {'node': 'moximoz', 417 | 'size': '9G', 418 | 'vmid': 201} 419 | }, 420 | }, 421 | { 422 | 'mp0': { 423 | 'storage': 'tank_multimedia', 424 | 'volume': 'subvol-201-disk-1', 425 | 'mp': '/multimedia', 426 | 'acl': True, 427 | 'shared': True, 428 | 'size': '10G', 429 | }, 430 | '__disk_resize': { 431 | 'mp0': {'node': 'moximoz', 'size': '10G', 'vmid': 201, } 432 | } 433 | }, 434 | ] 435 | 436 | CHANGE_DATA = { 437 | '201 basic': { 438 | 'state': 'present', 439 | 'vmid': 201, 440 | 'hostname': 'VM201', 441 | 'memory': 512, 442 | 'cores': 1, 443 | 'cpulimit': '0.4', 444 | # 'disk': '4G', 445 | # 'swap': 512, 446 | # 'storage': 'local-zfs', 447 | # 'ostemplate': '/var/lib/vz/template/cache/centos-7-default_20161207_amd64.tar.xz', 448 | # "mp0": "tank_multimedia:subvol-201-disk-1,mp=/multimedia,acl=1,size=8G", 449 | # TBD type=veth 450 | # "net0": "name=eth0,bridge=vmbr0,gw=192.168.178.1,ip=192.168.178.232/24,type=veth", 451 | # 'onboot': False, 452 | # 'delete': [cores, cpulimit], 453 | # '': '', 454 | }, 455 | '201 rootfs': { 456 | 'state': 'present', 457 | 'vmid': 201, 458 | 'hostname': 'VM201', 459 | 'memory': 512, 460 | 'rootfs': {'storage': 'local-zfs', 461 | 'volume': 'subvol-201-disk-1', 'size': '7G'}, 462 | # 'ostemplate': '/var/lib/vz/template/cache/centos-7-default_20161207_amd64.tar.xz', 463 | # 'storage': 'local-zfs', 464 | }, 465 | '201 net0': { 466 | "net0": {'name': 'eth0', 'bridge': 'vmbr0', 'gw': '192.168.178.1', 467 | 'ip': '192.168.111.111/24'}, 468 | }, 469 | '201 root resize': { 470 | 'rootfs': {'size': '9G'}, 471 | }, 472 | '201 mp0 resize': { 473 | 'mp0': { 474 | 'storage': 'tank_multimedia', 475 | 'volume': 'subvol-201-disk-1', 476 | 'mp': '/multimedia', 477 | 'acl': True, 478 | 'shared': True, 479 | 'size': '10G', 480 | }, 481 | }, 482 | '201 mp1 create': { 483 | 'mp1': { 484 | 'storage': 'tank_multimedia', 485 | 'volume': 'subvol-201-disk-2', 486 | 'mp': '/muhmuh', 487 | 'acl': True, 488 | # 'size': '10G', 489 | }, 490 | }, 491 | '201 multi resize': { 492 | 'rootfs': {'size': '9G'}, 493 | 'mp0': { 494 | 'storage': 'tank_multimedia', 495 | 'volume': 'subvol-201-disk-1', 496 | 'mp': '/multimedia', 497 | 'acl': True, 498 | 'size': '10G', 499 | }, 500 | }, 501 | '201 multi create': { 502 | 'mp0': { 503 | 'storage': 'tank_multimedia', 504 | 'volume': 'subvol-201-disk-1', 505 | 'mp': '/multimedia', 506 | 'acl': True, 507 | 'size': '10G', 508 | }, 509 | 'mp1': { 510 | 'storage': 'tank_multimedia', 511 | 'volume': 'subvol-201-disk-2', 512 | 'mp': '/muhmuh', 513 | 'acl': True, 514 | # 'size': '10G', 515 | }, 516 | }, 517 | } 518 | 519 | 520 | def mergedict(x, y): 521 | """Given two dicts, merge them into a new dict as a shallow copy.""" 522 | z = x.copy() 523 | z.update(y) 524 | return z 525 | 526 | 527 | @pytest.fixture(autouse=True) 528 | def patch_functions(monkeypatch): 529 | monkeypatch.setattr('subprocess.check_output', subprocess_mock) 530 | 531 | 532 | def subprocess_mock(cmd, shell=True, stderr=''): 533 | cmd_lines = cmd.split() 534 | action = cmd_lines[1] 535 | url = cmd_lines[2] 536 | all_params = cmd_lines[3:] 537 | # assume action is always ok. 538 | API = API_GET 539 | if action == 'set': 540 | API = API_SET 541 | if action == 'create': 542 | API = API_CREATE 543 | if action == 'delete': 544 | API = API_DELETE 545 | 546 | err_output = 'no \'{0}\' handler for {1} '.format(action, url) 547 | rc = 1 548 | if url in API: 549 | try: 550 | API[url].get('error') 551 | err_output = API[url].get('output', err_output) 552 | rc = API[url].get('rc', rc) 553 | raise subprocess.CalledProcessError(rc, cmd, err_output) 554 | except AttributeError: 555 | return API[url] 556 | else: 557 | raise KeyError('No url in fake API: ' + action + ' ' + url) 558 | 559 | return err_output 560 | -------------------------------------------------------------------------------- /library/proxmox_prov.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ANSIBLE_METADATA = {'status': ['preview'], 4 | 'supported_by': 'community', 5 | 'version': '0.8'} 6 | 7 | DOCUMENTATION = ''' 8 | --- 9 | module: proxmox_prov 10 | short_description: management of LXC instances on Proxmox VE 11 | description: 12 | - has no dependencies and directly uses the local pvesh API interface on the host. 13 | - current status: "works for me" 14 | - allows you to create/delete/stop LXC instances in Proxmox VE cluster 15 | - automatically creates container rootfs/mpX volumes via API if inexistent. 16 | - can resize rootfs/mpX volumes. 17 | - currently supports only lxc containers. 18 | - other storage configurations other than ZFS are untested. 19 | - currently it is only tested on a single PVE host, not part of a cluster. 20 | - For options reference: https://pve.proxmox.com/wiki/Manual:_pct.conf 21 | version_added: "2.1" 22 | options: 23 | vmid: 24 | description: 25 | - the instance id 26 | - vm creation: if not set, the next available ID will be fetched from API. 27 | - if not set, will be fetched from PromoxAPI based on the hostname 28 | default: null 29 | required: false 30 | password: 31 | description: 32 | - the instance root password 33 | - required only for new CTs and C(state=present) 34 | default: null 35 | required: false 36 | hostname: 37 | description: 38 | - the instance hostname 39 | - required only for C(state=present) 40 | - must be unique if vmid is not passed 41 | default: null 42 | required: false 43 | ostemplate: 44 | description: 45 | - the template for VM creation 46 | - required only for C(state=present) 47 | - can be a absolute file path, or pvesm format 'local:vztmpl/template.tar.xz ' 48 | default: null 49 | required: false 50 | arch: 51 | description: 52 | - OS architecture type (default=amd64) 53 | default: null 54 | required: false 55 | choices: ['amd64', 'i386'] 56 | cmode: 57 | description: 58 | - Console mode (default=tty) 59 | default: null 60 | required: false 61 | choices: ['console', 'shell', 'tty'] 62 | console: 63 | description: 64 | - Attach a console device (/dev/console) to the container. (default=1) 65 | type: bool 66 | default: null 67 | required: false 68 | cores: 69 | description: 70 | - numbers of allocated cores for instance (default=all) 71 | type: int 72 | default: null 73 | required: false 74 | cpulimit: 75 | description: 76 | - limit of cpu usage (default=0) 77 | type: int 78 | default: null 79 | required: false 80 | cpuunits: 81 | description: 82 | - CPU weight for a VM, relative to all other VMs. (default=1024) 83 | - larger value means more CPU time 84 | type: int 85 | default: null 86 | required: false 87 | delete: 88 | description: 89 | - A list of options that should be unset/deleted from the VM configuration. 90 | type: list 91 | default: null 92 | required: false 93 | description: 94 | description: 95 | - CT description as seen in web interface. 96 | type: string 97 | default: null 98 | required: false 99 | force: 100 | description: 101 | - forcing operations, untested, currently can be used only with C(present) 102 | - # TBD can be used only with states C(present), C(stopped), C(restarted) 103 | - # TBD with C(state=present) force option allow to overwrite existing container 104 | - # TBD with states C(stopped) , C(restarted) allow to force stop instance 105 | default: false 106 | required: false 107 | type: boolean 108 | lock: 109 | description: 110 | - Lock/Unlock the VM. 111 | type: enum 112 | default: null 113 | required: false 114 | choices: ['migrate', 'backup', 'snapshot', 'rollback'] 115 | memory: 116 | description: 117 | - memory size in MB for instance 118 | default: 512 119 | required: false 120 | mp[X]: 121 | description: 122 | - X is one of 0...9 123 | - specifies additional mounts (separate disks) for the container 124 | - > 125 | mp[n]: [volume=] ,mp= [,acl=<1|0>] [,backup=<1|0>] [,quota=<1|0>] 126 | [,ro=<1|0>] [,shared=<1|0>] [,size=(M|G|T)] 127 | - <1|0> means True/False boolean values 128 | - For a dir mount use {volume: /mnt/dir, mp: /mountinct, size: 0T}, size must be 0T. 129 | - For a bind mount use {volume: /mnt/bindmount, mp: /bindmount}. 130 | - Bind mount is not listed by df inside CT. 131 | default: null 132 | required: false 133 | type: A hash/dictionary defining mount point properties 134 | nameserver: 135 | description: 136 | - sets nameserver(s) for VM, format: "10.0.0.1 10.0.0.2 10.0.0.3" 137 | type: string 138 | default: null 139 | required: false 140 | net[X]: 141 | description: 142 | - X is one of 0...9 143 | - specifies one network interface netX for the container 144 | - > 145 | net[X]: name= [,bridge=] [,firewall=<1|0>] [,gw=] 146 | [,gw6=] [,hwaddr=] [,ip=] 147 | [,ip6=] [,mtu=] [,rate=] [,tag=] 148 | [,trunks=] [,type=] 149 | - enclose integer values in quotes 150 | - <1|0> means True/False boolean values 151 | default: null 152 | required: false 153 | type: A hash/dictionary defining interface properties 154 | node: 155 | description: 156 | - Proxmox VE node, when new VM will be created 157 | - if not set for C(state=present) uses local node hostname 158 | - for other states will be autodiscovered 159 | default: null 160 | required: false 161 | onboot: 162 | description: 163 | - specifies whether a VM will be started during system bootup (default=false) 164 | default: null 165 | required: false 166 | type: boolean 167 | ostype: 168 | description: 169 | - OS type. This is used to setup configuration inside the container... 170 | type: enum 171 | default: null 172 | required: false 173 | choices: ['alpine', 'archlinux', 'centos', 'debian', 'fedora', 'gentoo', 174 | 'opensuse', 'ubuntu', 'unmanaged'] 175 | pool: 176 | description: 177 | - add VM to Proxmox VE resource pool 178 | - Applies only to newly created VMs and is ignored for config runs. 179 | type: str 180 | default: null 181 | required: false 182 | protection: 183 | description: 184 | - Sets protection flag of container. 185 | type: bool 186 | default: null 187 | required: false 188 | rootfs: 189 | description: 190 | - the container root filesystem 191 | - for C(state=present): if not set, a volume will be automatically created. 192 | - > 193 | options: [storage=] [volume=] [acl=<1|0>] [,quota=<1|0>] 194 | [,ro=<1|0>] [,shared=<1|0>] [,size=(M|G|T)] 195 | - <1|0> means True/False boolean values 196 | - default: (zfs) volume name= subvol--disk-1, size 4G 197 | - > 198 | volume can be defined as {'volume': 'local-zfs:subvol-101-disk-1'} or 199 | {'storage': '...', 'volume': '...'} 200 | default: null 201 | required: false 202 | type: A hash/dictionary defining rootfs properties 203 | searchdomain: 204 | description: 205 | - The container DNS search domain (name). 206 | type: str 207 | default: null 208 | required: false 209 | ssh-public-keys: 210 | description: 211 | - On CT create setup ssh public key for root, supports only one key currently. 212 | type: str 213 | default: null 214 | required: false 215 | startup: 216 | description: 217 | - \[order=]\d+ [,up=\d+] [,down=\d+] 218 | - can also be just a number defining the startup order. 219 | type: str 220 | default: null 221 | required: false 222 | state: 223 | description: 224 | - Indicate desired state of the instance 225 | choices: ['present', 'absent', 'start', 'stop', 'restart'] 226 | default: present 227 | storage: 228 | description: 229 | - only relevant for new VM creation with C(state=present) 230 | - target storage if not rootfs defined 231 | type: string 232 | default: 'local' 233 | required: false 234 | swap: 235 | description: 236 | - amount of swap in MB. 237 | type: int 238 | default: 512 239 | required: false 240 | # default: null # tbd 241 | template: 242 | description: 243 | - Enable/disable template. 244 | - does not yet seem to be implemented. 245 | type: bool 246 | default: null 247 | required: false 248 | timeout: 249 | description: 250 | - timeout for shutdown operations, cannot be used in C(state=present). 251 | default: 60 252 | required: false 253 | type: integer 254 | tty: 255 | description: 256 | - The number of tty (0-6) available to the container. 257 | type: int 258 | default: null 259 | required: false 260 | unprivileged: 261 | description: 262 | - Makes the container run as unprivileged user. (Should not be modified manually.) 263 | - Can only be used on newly created CTs, 264 | type: bool 265 | default: null 266 | required: false 267 | notes: 268 | - > 269 | Uses pvesh command on the host node, meaning the executing (ansible) user 270 | needs sudo privileges. 271 | - Or the user has the appropriate permissions set in Proxmox VE. (untested) 272 | requirements: [ "python >= 2.7" ] 273 | author: "mozit.eu" 274 | ''' 275 | 276 | EXAMPLES = ''' 277 | # Create new container with minimal options 278 | - proxmox_prov: 279 | vmid: 100 280 | password: 123456 281 | hostname: example.org 282 | ostemplate: 'local:vztmpl/centos-7-default_20161207_amd64.tar.xz' 283 | # Create new container with some options and network interface with static ip. 284 | - proxmox_prov: 285 | vmid: 100 286 | node: mo03 287 | password: 123456 288 | hostname: example.org 289 | ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' 290 | net0: 291 | name: eth0 292 | ip: 192.168.0.100/24 293 | gw: 192.168.0.1 294 | bridge: vmbr0 295 | # Create new container with some options and network interface with dhcp. 296 | - proxmox_prov: 297 | vmid: 100 298 | node: mo03 299 | password: 123456 300 | hostname: example.org 301 | ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' 302 | net0: 303 | name: eth0 304 | ip: dhcp 305 | ip6: dhcp 306 | bridge: vmbr0 307 | # Create new container, defining rootfs and a mount, which will be created if inexistent 308 | - proxmox_prov: 309 | vmid: 100 310 | node: mo03 311 | password: 123456 312 | hostname: example.org 313 | ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' 314 | mp0: 315 | # must be defined as proxmox storage pool 316 | storage: tank_media 317 | volume: subvol-100-disk-2 318 | mp: /mnt/media 319 | acl: True 320 | size: 5G 321 | rootfs: 322 | storage: local-zfs 323 | volume: subvol-201-disk-1 324 | size: 7G 325 | # Start container 326 | - proxmox_prov: 327 | vmid: 100 328 | state: start 329 | # Shutdown/Stop container, with optional timeout 330 | - proxmox_prov: 331 | vmid: 100 332 | state: shutdown 333 | timeout: 90 334 | # Stop container with force 335 | - proxmox_prov: 336 | vmid: 100 337 | state: stop 338 | # Restart container 339 | - proxmox_prov: 340 | vmid: 100 341 | state: restart 342 | # Remove container 343 | - proxmox_prov: 344 | vmid: 100 345 | state: absent 346 | ''' 347 | 348 | 349 | import subprocess 350 | import socket 351 | import json 352 | from ansible.module_utils.basic import AnsibleModule 353 | 354 | # Constants 355 | '''These Params are unknown to the API and will not be passed to the api by pvesh''' 356 | API_PARAM_FILTER = ['node', 'state', 'ssh_public_keys'] 357 | '''Some (simple) params may need conversion from their YAML counterparts.''' 358 | API_PARAM_CONV = {'onboot': int, 'delete': ','.join, 359 | # disk/mp params 360 | 'acl': int, 'backup': int, 'quota': int, 'ro': int, 'shared': int, 361 | # netX 362 | 'firewall': int, 363 | # misc, the api always adds a final \n to description 364 | 'console': int, 'force': int, 'description': lambda x: x.rstrip(), 365 | 'template': int, 'unprivileged': int 366 | } 367 | '''These params are unknown to config API call, only used in creation.''' 368 | API_PARAM_CREATE_ONLY = ['ostemplate', 'storage', 'vmid', 'password', 'pool', 369 | 'ssh-public-keys', 'unprivileged'] 370 | '''These are unknown to create, only used in config.''' 371 | API_PARAM_CONFIG_ONLY = ['delete'] 372 | 373 | '''These are the valid network and mountpoint key names''' 374 | NET_NAMES = ['net' + str(x) for x in range(0, 10)] 375 | MP_NAMES = ['mp' + str(x) for x in range(0, 10)] + ['rootfs'] 376 | 377 | 378 | def pvesh(action='get', url='/', data=dict()): 379 | cmd = 'pvesh {0} {1} --output-format json'.format(action, url) 380 | if data: 381 | cmd = 'pvesh {0} {1} {2} --output-format json'.format(action, url, 382 | get_api_format(data)) 383 | try: 384 | out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) 385 | dic = get_api_status_and_data(out, True, 0) 386 | dic.update({'rc': 0, 'cmd': cmd}) 387 | return dic 388 | # return {'status': status, 'status_code': status_code, 'data': data} 389 | except subprocess.CalledProcessError as e: # thrown if non-zero exit code 390 | err = e.output 391 | # print(err.decode('utf-8').strip() + ' : ' + cmd) 392 | dic = get_api_status_and_data(err, False, e.returncode) 393 | dic.update({'rc': e.returncode, 'cmd': cmd}) 394 | return dic 395 | 396 | # no first line like "200 OK" since 5.? 397 | def get_api_status_and_data(rawdata, json_fmt=True, exitcode=255): 398 | rawdata = rawdata.strip() 399 | status_code = exitcode 400 | if exitcode == 0: # fake pre 5.x behaviour with http codes 401 | status_code = 200 402 | if(status_code == 200): 403 | status = 'OK' 404 | else: 405 | status = 'FAIL' 406 | if json_fmt: 407 | # catch case where pvesh does not return json data. 408 | try: 409 | data = json.loads(rawdata) 410 | except ValueError: 411 | data = rawdata 412 | else: 413 | data = rawdata 414 | return {'status_code': status_code, 'status': status, 'data': data} 415 | 416 | 417 | def get_api_format(data, conv=API_PARAM_CONV, fmt='pvesh'): 418 | '''Get a string with the parameters in the format the api expects. 419 | ''' 420 | arglist = [] 421 | filtered = get_api_dict(data) 422 | for k, v in filtered.items(): 423 | # data conversion, i.e. concat delete list 424 | v = conv[k](v) if k in conv else v 425 | # quote value if contains space 426 | arg = "'" + str(v) + "'" if ' ' in str(v) else str(v) 427 | arglist.append(k + '=' + arg) 428 | params = '-' + ' -'.join(arglist) 429 | return params 430 | 431 | 432 | def get_api_dict(data, filter=API_PARAM_FILTER): 433 | '''Get a new dictionary without not-api keys, 434 | Also remove null values''' 435 | filtered_dict = dict() 436 | for k, v in data.items(): 437 | if k not in filter: 438 | if v is not None: # do not pass on unset (null) values 439 | filtered_dict[k] = v 440 | return filtered_dict 441 | 442 | 443 | def get_dict_diff(A, B): 444 | '''Compare A to B and return all keys-value pairs in A that differ from B.''' 445 | diff = {} 446 | sa = set(A.items()) 447 | sb = set(B.items()) 448 | sdiff = sa ^ sb # set symmetric_difference 449 | keys = set([x for x, y in sdiff]) # all keys different in A or B 450 | for k in keys: 451 | if k in A: 452 | diff[k] = A[k] 453 | return diff 454 | 455 | 456 | def get_cluster_resources(): 457 | result = pvesh(action='get', url='/cluster/resources') 458 | return result 459 | 460 | 461 | def get_cluster_vms(resource_list): 462 | return [item for item in resource_list if item['type'] == 'lxc'] 463 | 464 | 465 | def get_storage_volumes(node, storage): 466 | '''Get all volumes on this storage on this node''' 467 | result = pvesh(action='get', url='/nodes/{0}/storage/{1}/content' 468 | .format(node, storage)) 469 | return result 470 | 471 | 472 | def get_volume(node, storage, volid): 473 | '''Search list from get_storage_volumes for item with volid or name.''' 474 | result = get_storage_volumes(node, storage) 475 | if 'status' in result and result['status'] == 'FAIL': 476 | print("API call failed: " + str(result['data'])) 477 | raise RuntimeError("API call failed: " + str(result['data'])) 478 | 479 | volumes = result['data'] 480 | for item in volumes: 481 | if 'volid' in item and volid == item['volid']: 482 | return item 483 | elif 'name' in item and volid == item['name']: 484 | return item 485 | return None 486 | 487 | 488 | def get_volume_params(vol): 489 | '''From a dict (from ansible), get (splitted) volume parameters: 490 | storage, format, volumename(filename), size. 491 | vol["storage"] is optional if volume is "local:subvol-100-diskx". ''' 492 | sto, fmt, volname, sz = (None, None, None, None) 493 | # TBD Could use a global DEFAULT_VOL_SIZE here 494 | sz = vol['size'] if 'size' in vol else '4G' 495 | # Problem: if user uses '0' for size, the API still saves it as '0T'. 496 | # HACK patch size 0 special case for API, always use 0G 497 | sz = '0G' if sz == '0' else sz 498 | sto = vol['storage'] if 'storage' in vol else None 499 | if 'volume' in vol: 500 | if vol['volume'].startswith('/'): # plain directory case 501 | volname = vol['volume'] 502 | else: 503 | # Could extract fmt from volume name too, TBD how are raw and qcow2 vols named? 504 | fmt = vol['format'] if 'format' in vol else 'subvol' 505 | if ':' in vol['volume']: 506 | sto, volname = vol['volume'].split(':') 507 | elif 'storage' in vol: # case both storage and volume 508 | sto, volname = (vol['storage'], vol['volume']) 509 | return (sto, fmt, volname, sz) 510 | 511 | 512 | def get_vm_by_id(id, resource_list=None): 513 | '''Get VM data from the cluster resource list, which can be provided 514 | as second parameter.''' 515 | vm = None 516 | if not resource_list: 517 | resource_list = get_cluster_vms(get_cluster_resources().get('data', None)) 518 | for item in resource_list: 519 | if 'vmid' in item and int(item['vmid']) == int(id): 520 | vm = item 521 | return vm 522 | 523 | 524 | def get_vm_by_hostname(name, resource_list=None): 525 | '''Get VM data from the cluster resource list by name''' 526 | vm = None 527 | if not resource_list: 528 | resource_list = get_cluster_vms(get_cluster_resources().get('data', None)) 529 | for item in resource_list: 530 | # in the resource list the hostname is called 'name' 531 | if item.get('name') == name: 532 | vm = item 533 | return vm 534 | 535 | 536 | def get_vm_config(vm): 537 | '''Get detailed vm config from the api, vm is a single item from the resource list. 538 | result.data contains the config.''' 539 | url = '/nodes/{0}/lxc/{1}/config'.format(vm['node'], vm['vmid']) 540 | result = pvesh('get', url) 541 | # Add node to vm config, not a true config item but needed later, by volume handling. 542 | if 'data' in result: 543 | result['data']['node'] = vm['node'] 544 | # HACK patch data, the api always adds \n to the description string, remove. 545 | if 'description' in result['data']: 546 | result['data']['description'] = result['data']['description'].rstrip() 547 | return result 548 | 549 | 550 | def set_vm_config(vm, changes): 551 | '''Set the vm configuration via API. converts disk/net dicts to api format.''' 552 | url = '/nodes/{0}/lxc/{1}/config'.format(vm['node'], vm['vmid']) 553 | # At last convert disks to correct format 554 | for k in list(changes.keys()): # should be a safe way to iterate and change the dict. 555 | if k in MP_NAMES: 556 | changes[k] = convert_params(changes[k]) 557 | if k in NET_NAMES: 558 | changes[k] = convert_params(changes[k]) 559 | 560 | result = pvesh('set', url, changes) 561 | # api set config does not return anything useful. 562 | return result 563 | 564 | 565 | def get_vm_disk_size(vm, disk='rootfs'): 566 | '''DEPRECATED Returns 0 if disk has no size property, and None if the disk does not exist''' 567 | if vm and disk in vm: 568 | items = vm[disk].split(',') 569 | for x in items: 570 | if x.startswith('size'): 571 | return x.split('=')[1] 572 | return 0 573 | else: 574 | return None 575 | 576 | 577 | def convert_params(thing): 578 | '''Depending on input convert to PVE disk/net string or ansible disk/net dict''' 579 | # We need the list of params for an ordered, predictable (testable) output string. 580 | disk_params = ['mp', 'acl', 'backup', 'quota', 'ro', 'shared', 'size'] 581 | # skip name, see elif below 582 | net_params = ['bridge', 'firewall', 'gw', 'gw6', 'hwaddr', 'ip', 583 | 'ip6', 'mtu', 'rate', 'tag', 'trunks', 'type'] 584 | params = disk_params + net_params 585 | # Ask forgiveness not permission 586 | try: # Dict case: convert dict to pve string 587 | r = '' 588 | if 'volume' in thing and thing['volume'].startswith('/'): # disk directory 589 | r += thing['volume'] 590 | elif 'storage' in thing and 'volume' in thing: # disk volume case 591 | r += thing['storage'] + ':' + thing['volume'] 592 | elif 'name' in thing: # Network interface case 593 | r += 'name=' + thing['name'] 594 | 595 | for p in params: 596 | if p in thing and p not in ('storage', 'volume'): 597 | v = API_PARAM_CONV[p](thing[p]) if p in API_PARAM_CONV else thing[p] 598 | r += ',{0}={1}'.format(p, str(v)) 599 | return r 600 | except TypeError: # String case: convert pve string to dict 601 | r = {} 602 | lstart = 0 603 | lines = thing.split(',') 604 | if not thing.startswith('name='): # netX starts with name param 605 | if thing.startswith('/'): # disk is directory 606 | r['volume'] = lines[lstart] 607 | else: 608 | r['storage'], r['volume'] = lines[lstart].split(':') 609 | lstart += 1 610 | for p in lines[lstart:]: 611 | k, v = p.split('=') 612 | # # HACK api patch, a size of 0 could be returned as 0T by API. 613 | # if k == "size" and v in ['0M', '0G', '0T']: 614 | # v = '0' 615 | # currently all this params are int, so simply applying int is ok. 616 | v = bool(int(v)) if k in API_PARAM_CONV.keys() else v 617 | r[k] = v 618 | return r 619 | # should not happen 620 | return None 621 | 622 | 623 | def get_vm_changes(api_data, vm_config, apply_filter=False): 624 | data = api_data 625 | changes = dict() 626 | 627 | for k, v in data.items(): 628 | if k == 'delete' and v: 629 | for item in v: # Only add delete item if exists in config 630 | if item in vm_config: 631 | if 'delete' not in changes: 632 | changes['delete'] = list() 633 | changes['delete'].append(item) 634 | elif k in MP_NAMES and v: 635 | if k not in vm_config: # the disk is new, create 636 | (sto, fmt, volname, sz) = get_volume_params(v) 637 | if get_volume(vm_config['node'], sto, volname) is None and not volname.startswith('/'): 638 | create = {k: { 639 | 'node': vm_config['node'], 640 | 'storage': sto, 'vmid': data['vmid'], 'format': fmt, 641 | 'filename': volname, 'size': sz}} 642 | if '__disk_create' not in changes: 643 | changes['__disk_create'] = dict() 644 | changes['__disk_create'].update(create) 645 | # changes.update(create) 646 | changes.update({k: v}) 647 | else: 648 | diff = get_dict_diff(v, convert_params(vm_config[k])) 649 | # do not resize to 0, dir mounts have 0 size 650 | if 'size' in diff and diff['size'] not in [0, '0', '0M', '0G', '0T']: 651 | resize = {k: { 652 | 'node': vm_config['node'], 653 | 'vmid': data['vmid'], 654 | 'size': diff['size'], 655 | }} 656 | if '__disk_resize' not in changes: 657 | changes['__disk_resize'] = dict() 658 | changes['__disk_resize'].update(resize) 659 | if diff: 660 | # make sure existing disk params are not deleted by changes 661 | newdiff = convert_params(vm_config[k]) 662 | newdiff.update(diff) 663 | changes.update({k: newdiff}) 664 | # get the params from v 665 | elif k in NET_NAMES and v: 666 | if k in vm_config: 667 | net_cfg = convert_params(vm_config[k]) 668 | else: 669 | net_cfg = dict() 670 | diff = get_dict_diff(v, net_cfg) 671 | if diff: 672 | # include already set values 673 | net_cfg.update(diff) 674 | changes.update({k: net_cfg}) 675 | elif k in vm_config: 676 | if v != vm_config[k]: # existing option value mismatch, change 677 | changes[k] = v 678 | 679 | else: # set new config option 680 | changes[k] = v 681 | 682 | if apply_filter: 683 | # should be ok to use this default here, because only vm_config uses changes. 684 | filter = list(set(API_PARAM_FILTER + API_PARAM_CREATE_ONLY)) 685 | changes = get_api_dict(changes, filter) 686 | return changes 687 | 688 | 689 | def create_volume(node, storage, vmid, format='subvol', volname=None, size='4G'): 690 | if volname is None: 691 | volname = '{0}-{1}-disk-1'.format(format, vmid) 692 | data = {'vmid': vmid, 'format': format, 'size': size, 'filename': volname} 693 | query = '/nodes/{0}/storage/{1}/content'.format(node, storage) 694 | result = pvesh('create', query, data) 695 | return result 696 | 697 | 698 | def resize_volume(node, vmid, disk, size): 699 | data = {'disk': disk, 'size': size} 700 | query = '/nodes/{0}/lxc/{1}/resize'.format(node, vmid) 701 | result = pvesh('set', query, data) 702 | return result 703 | 704 | 705 | def create_vm(data): 706 | '''Create a simple VM with a rootfs, to fully configure the created VM 707 | one has to use config_vm on the newly created VM.''' 708 | # no ID specified, get new id from cluster 709 | # in this case the json data is simply a string like "102" 710 | if 'vmid' not in data or not data['vmid']: 711 | result = pvesh('get', url='/cluster/nextid') 712 | if result.get('status_code', None) == 200: 713 | data['vmid'] = int(result['data']) 714 | else: 715 | return result 716 | vmid = data['vmid'] 717 | # get node from ansible or data 718 | if not data.get('node', None): 719 | data['node'] = socket.gethostname() 720 | filtered_data = get_api_dict(data, filter=API_PARAM_CONFIG_ONLY) 721 | 722 | # If rootfs is fully specd, we create or own disk. If not we let proxmox do it 723 | # with default params (size), the other params are set by config later. 724 | # create rootfs disk. disk create failures not handled yet 725 | fs = filtered_data.get('rootfs', None) 726 | if fs: 727 | storage, fmt, volname, sz = get_volume_params(fs) 728 | if storage and volname: 729 | filtered_data.pop('storage', None) # remove now unnecessary param from data 730 | if get_volume(data['node'], storage, volname) is None: 731 | result = create_volume(data['node'], storage, data['vmid'], 732 | fmt, volname, sz) 733 | # An error occured 734 | if result.get('status_code', None) != 200: 735 | result.update({'status': 'FAIL'}) 736 | return result 737 | # finally convert rootfs dict to PVE format 738 | filtered_data['rootfs'] = convert_params(fs) 739 | else: 740 | # if only rootfs.storage defined, move it to main param list. 741 | if storage: 742 | filtered_data.update({'storage': storage}) 743 | filtered_data.pop('rootfs', None) # remove incomplete rootfs for create 744 | 745 | # parameters like netX and mpX are handled by config_vm, remove for create. 746 | for x in NET_NAMES + MP_NAMES: 747 | if x != 'rootfs': 748 | filtered_data.pop(x, None) 749 | 750 | result = pvesh('create', '/nodes/{0}/lxc'.format(data['node']), filtered_data) 751 | if result.get('status_code', None) == 200: 752 | result.update({'vmid': vmid}) 753 | # more items are not really necessary 754 | # result.update({'action': 'create', 'create': 'success', 'vmid': vmid}) 755 | return result 756 | 757 | 758 | def config_vm(data, vm): 759 | '''Configure a vm if the config params do not match the ones in data''' 760 | # get vm current config 761 | result = get_vm_config(vm) 762 | # Error check 763 | if result['status_code'] != 200: 764 | result.update(changed=False) 765 | return result 766 | vm_config = result['data'] 767 | change_items = get_vm_changes(data, vm_config, True) 768 | api_items = get_api_dict(change_items, 769 | filter=list(set(API_PARAM_FILTER + API_PARAM_CREATE_ONLY))) 770 | if api_items: 771 | # Handle disk resize and create 772 | if '__disk_create' in api_items: 773 | items = api_items.pop('__disk_create') 774 | for disk, cfg in items.items(): 775 | result = create_volume(cfg['node'], cfg['storage'], 776 | cfg['vmid'], cfg['format'], cfg['filename'], 777 | cfg['size']) 778 | if result['status_code'] != 200: 779 | return result 780 | if '__disk_resize' in api_items: 781 | items = api_items.pop('__disk_resize') 782 | for disk, cfg in items.items(): 783 | result = resize_volume(cfg['node'], cfg['vmid'], disk, cfg['size']) 784 | if result['status_code'] != 200: 785 | return result 786 | 787 | # Set VM config options 788 | ret = set_vm_config(vm, api_items) 789 | # Always return a status_code 790 | if 'status_code' not in ret: 791 | # TODO if meta in ret, rc == 0 792 | ret.update(status_code=200) 793 | ret.update(changed=True, changes=change_items) 794 | return ret 795 | else: 796 | result.update(changed=False) 797 | return result 798 | 799 | 800 | def pve_ct_present(data): 801 | 802 | all_results = get_cluster_resources() 803 | vms = get_cluster_vms(all_results['data']) 804 | # get vm by ID or hostname 805 | if data['vmid']: 806 | vm = get_vm_by_id(data['vmid'], vms) 807 | else: 808 | vm = get_vm_by_hostname(data['hostname'], vms) 809 | # If vm is null goto create 810 | if vm: 811 | result = config_vm(data, vm) 812 | error = False if result['status_code'] == 200 else True 813 | # The changed key should always be set, but to be sure check that. 814 | changed = result['changed'] if 'changed' in result else True 815 | return error, changed, result 816 | else: 817 | result = create_vm(data) 818 | error = False if result['status_code'] == 200 else True 819 | if not error: 820 | vmid = result.get('vmid', None) 821 | if vmid: 822 | vm = get_vm_by_id(vmid) 823 | cfg_result = config_vm(data, vm) 824 | cfg_result.update({'create': result}) 825 | return error, True, cfg_result 826 | 827 | return error, True, result 828 | 829 | # default: something went wrong 830 | meta = {'present': 'ERROR', "status": all_results['status'], 'response': all_results} 831 | return True, False, meta 832 | 833 | 834 | def pve_ct_absent(data): 835 | all_results = get_cluster_resources() 836 | vms = get_cluster_vms(all_results['data']) 837 | # get vm by ID or hostname 838 | if data['vmid']: 839 | vm = get_vm_by_id(data['vmid'], vms) 840 | else: 841 | vm = get_vm_by_hostname(data['hostname'], vms) 842 | # If vm == null, do nothing 843 | # double check id match, of none given check hostname too 844 | if vm and (vm['vmid'] == data['vmid'] 845 | or (not data['vmid'] and vm['name'] == data['hostname'])): 846 | # node should really be in vm['node'], but if node use local node 847 | node = None 848 | if 'node' in vm and vm['node']: 849 | node = vm['node'] 850 | else: 851 | node = socket.gethostname() 852 | result = pvesh('delete', '/nodes/{0}/lxc/{1}' 853 | .format(node, vm['vmid'])) 854 | error = False if result['status_code'] == 200 else True 855 | return error, True, result 856 | else: 857 | meta = {"absent": "VM does not exist."} 858 | return False, False, meta 859 | 860 | meta = {"absent": "ERROR", "status": all_results['status'], 861 | 'response': all_results} 862 | return True, False, meta 863 | 864 | 865 | def get_vm_id(data): 866 | '''Get the int vmid of a vm. data can be a dict or just a hostname.''' 867 | try: # its a dict 868 | id = data.get('vmid') 869 | if id: 870 | return id 871 | name = data.get('hostname') 872 | except AttributeError: # its a hostname string 873 | name = data 874 | vm = get_vm_by_hostname(name) 875 | if vm: 876 | return vm.get('vmid') 877 | return None 878 | 879 | 880 | def get_vm_status(id, node=None): 881 | '''id is the int id or None.''' 882 | # vmid = int(id) 883 | vmid = id 884 | # try: 885 | # vmid = int(id) 886 | # except ValueError: 887 | # try: 888 | # vmid = get_vm_by_hostname(id).get('vmid') 889 | # except AttributeError: 890 | # vmid = None 891 | if vmid and not node: 892 | vm = get_vm_by_id(vmid) 893 | node = vm.get('node') if vm else None 894 | if vmid and node: 895 | result = pvesh('get', '/nodes/{0}/lxc/{1}/status/current' 896 | .format(node, vmid)) 897 | if result.get('rc') == 0: 898 | state = 'unknown' 899 | try: 900 | state = result.get('data').get('status') 901 | except AttributeError: 902 | pass 903 | return state 904 | return None 905 | 906 | 907 | def set_vm_status(id, status, node=None, timeo=None): 908 | cmds = ['start', 'shutdown', 'stop', 'suspend', 'resume'] 909 | data = None 910 | if timeo: 911 | data = {'timeout': timeo} 912 | if status not in cmds: 913 | return {'status': 'FAIL', 'status_code': 400, 'data': 914 | 'unknown status: {0}'.format(status)} 915 | if not node: 916 | vm = get_vm_by_id(id) 917 | node = vm.get('node') if vm else None 918 | if id and node: 919 | result = pvesh('create', '/nodes/{0}/lxc/{1}/status/{2}' 920 | .format(node, id, status), data) 921 | return result 922 | return {'status': 'FAIL', 'status_code': 400, 'data': 'vm does not exist.'} 923 | 924 | 925 | def pve_ct_start(data): 926 | id = get_vm_id(data) 927 | status = get_vm_status(id, data.get('node')) 928 | 929 | if not status: 930 | meta = {'status': 'FAIL', 'data': 'VM {0} does not exist.'.format(id)} 931 | return True, False, meta 932 | 933 | if(status != 'running'): 934 | meta = set_vm_status(id, 'start', data.get('node')) 935 | return False, True, meta 936 | else: 937 | meta = {'status': 'OK', 'data': 'VM {0} is already running.'.format(id)} 938 | return False, False, meta 939 | 940 | # unknown error 941 | meta = {"status": "ERROR", "data": 'Unknown error.'} 942 | return True, False, meta 943 | 944 | 945 | def pve_ct_stop(data): 946 | id = get_vm_id(data) 947 | status = get_vm_status(id, data.get('node')) 948 | 949 | if not status: 950 | meta = {'status': 'FAIL', 'data': 'VM {0} does not exist.'.format(id)} 951 | return True, False, meta 952 | 953 | if(status != 'stopped'): 954 | meta = set_vm_status(id, 'stop', data.get('node')) 955 | return False, True, meta 956 | else: # TBD status suspended, but suspending does not work here. 957 | meta = {'status': 'OK', 'data': 'VM {0} is already stopped.'.format(id)} 958 | return False, False, meta 959 | 960 | 961 | def pve_ct_shutdown(data): 962 | id = get_vm_id(data) 963 | status = get_vm_status(id, data.get('node')) 964 | timeout = data.get('timeout') 965 | 966 | if not status: 967 | meta = {'status': 'FAIL', 'data': 'VM {0} does not exist.'.format(id)} 968 | return True, False, meta 969 | 970 | if(status != 'stopped'): 971 | meta = set_vm_status(id, 'shutdown', data.get('node'), timeo=timeout) 972 | return False, True, meta 973 | else: 974 | meta = {'status': 'OK', 'data': 'VM {0} is already stopped.'.format(id)} 975 | return False, False, meta 976 | 977 | 978 | def pve_ct_restart(data): 979 | id = get_vm_id(data) 980 | status = get_vm_status(id, data.get('node')) 981 | timeout = data.get('timeout') 982 | 983 | if not status: 984 | meta = {'status': 'FAIL', 'data': 'VM {0} does not exist.'.format(id)} 985 | return True, False, meta 986 | 987 | if(status == 'running'): 988 | meta = set_vm_status(id, 'shutdown', data.get('node'), timeo=timeout) 989 | meta = set_vm_status(id, 'start', data.get('node'), timeo=timeout) 990 | 991 | return False, True, meta 992 | 993 | 994 | def pve_ct_suspend(data): 995 | # suspending TBD 996 | meta = {"status": "ERROR", "data": 'Not implemented'} 997 | return True, False, meta 998 | 999 | 1000 | def pve_ct_resume(data): 1001 | # suspending TBD 1002 | meta = {"status": "ERROR", "data": 'Not implemented'} 1003 | return True, False, meta 1004 | 1005 | 1006 | def main(): 1007 | 1008 | # Proxmox API defaults are not set here, let the API deal with it. 1009 | module = AnsibleModule( 1010 | argument_spec=dict( 1011 | # unsorted: state, vmid, password, hostname, ostemplate 1012 | state=dict(default='present', type='str', 1013 | choices=['present', 'absent', 'start', 'shutdown', 'stop', 1014 | 'restart', 'suspend', 'resume']), 1015 | vmid=dict(type='int', required=False), 1016 | password=dict(type='str', no_log=True), 1017 | hostname=dict(type='str', required=False), 1018 | ostemplate=dict(type='str'), 1019 | arch=dict(type='str'), 1020 | cmode=dict(type='str'), 1021 | console=dict(type='bool'), 1022 | cores=dict(type='int'), 1023 | cpuunits=dict(type='int'), 1024 | delete=dict(type='list'), 1025 | description=dict(type='str'), 1026 | force=dict(type='bool'), 1027 | lock=dict(type='str'), 1028 | memory=dict(type='int'), 1029 | mp0=dict(type='dict'), 1030 | mp1=dict(type='dict'), 1031 | mp2=dict(type='dict'), 1032 | mp3=dict(type='dict'), 1033 | mp4=dict(type='dict'), 1034 | mp5=dict(type='dict'), 1035 | mp6=dict(type='dict'), 1036 | mp7=dict(type='dict'), 1037 | mp8=dict(type='dict'), 1038 | mp9=dict(type='dict'), 1039 | nameserver=dict(type='str'), 1040 | net0=dict(type='dict'), 1041 | net1=dict(type='dict'), 1042 | net2=dict(type='dict'), 1043 | net3=dict(type='dict'), 1044 | net4=dict(type='dict'), 1045 | net5=dict(type='dict'), 1046 | net6=dict(type='dict'), 1047 | net7=dict(type='dict'), 1048 | net8=dict(type='dict'), 1049 | net9=dict(type='dict'), 1050 | node=dict(type='str'), 1051 | onboot=dict(type='bool'), 1052 | # could be an enum according to docs, this is freeer. 1053 | ostype=dict(type='str'), 1054 | pool=dict(type='str'), 1055 | protection=dict(type='bool'), 1056 | rootfs=dict(type='dict'), 1057 | searchdomain=dict(type='str'), 1058 | # hyphen in var name not allowed, workaround and exclude _var_ 1059 | ssh_public_keys=dict(type='str', aliases=['ssh-public-keys']), 1060 | startup=dict(type='str'), 1061 | storage=dict(type='str'), 1062 | swap=dict(type='int', default=512), # enforce value as in API docs. 1063 | template=dict(type='bool'), 1064 | # timeout is only valid for start/stop operations 1065 | timeout=dict(type='int'), 1066 | tty=dict(type='int'), 1067 | unprivileged=dict(type='bool'), 1068 | ) 1069 | ) 1070 | choice_map = { 1071 | "present": pve_ct_present, 1072 | "absent": pve_ct_absent, 1073 | "start": pve_ct_start, 1074 | "shutdown": pve_ct_shutdown, 1075 | "stop": pve_ct_stop, 1076 | "restart": pve_ct_restart, 1077 | "suspend": pve_ct_suspend, 1078 | "resume": pve_ct_resume, 1079 | } 1080 | is_error, has_changed, result = choice_map.get(module.params['state'])(module.params) 1081 | 1082 | if not is_error: 1083 | module.exit_json(changed=has_changed, meta=result) 1084 | else: 1085 | module.fail_json(msg="Error in proxmox_prov module", meta=result) 1086 | 1087 | 1088 | if __name__ == '__main__': 1089 | main() 1090 | --------------------------------------------------------------------------------