1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# Copyright: Ansible Project
5#
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11
12DOCUMENTATION = '''
13---
14module: proxmox_template
15short_description: management of OS templates in Proxmox VE cluster
16description:
17  - allows you to upload/delete templates in Proxmox VE cluster
18options:
19  node:
20    description:
21      - Proxmox VE node on which to operate.
22    type: str
23  src:
24    description:
25      - path to uploaded file
26      - required only for C(state=present)
27    type: path
28  template:
29    description:
30      - the template name
31      - Required for state C(absent) to delete a template.
32      - Required for state C(present) to download an appliance container template (pveam).
33    type: str
34  content_type:
35    description:
36      - content type
37      - required only for C(state=present)
38    type: str
39    default: 'vztmpl'
40    choices: ['vztmpl', 'iso']
41  storage:
42    description:
43      - target storage
44    type: str
45    default: 'local'
46  timeout:
47    description:
48      - timeout for operations
49    type: int
50    default: 30
51  force:
52    description:
53      - can be used only with C(state=present), exists template will be overwritten
54    type: bool
55    default: 'no'
56  state:
57    description:
58     - Indicate desired state of the template
59    type: str
60    choices: ['present', 'absent']
61    default: present
62notes:
63  - Requires proxmoxer and requests modules on host. This modules can be installed with pip.
64author: Sergei Antipov (@UnderGreen)
65extends_documentation_fragment: community.general.proxmox.documentation
66'''
67
68EXAMPLES = '''
69- name: Upload new openvz template with minimal options
70  community.general.proxmox_template:
71    node: uk-mc02
72    api_user: root@pam
73    api_password: 1q2w3e
74    api_host: node1
75    src: ~/ubuntu-14.04-x86_64.tar.gz
76
77- name: >
78    Upload new openvz template with minimal options use environment
79    PROXMOX_PASSWORD variable(you should export it before)
80  community.general.proxmox_template:
81    node: uk-mc02
82    api_user: root@pam
83    api_host: node1
84    src: ~/ubuntu-14.04-x86_64.tar.gz
85
86- name: Upload new openvz template with all options and force overwrite
87  community.general.proxmox_template:
88    node: uk-mc02
89    api_user: root@pam
90    api_password: 1q2w3e
91    api_host: node1
92    storage: local
93    content_type: vztmpl
94    src: ~/ubuntu-14.04-x86_64.tar.gz
95    force: yes
96
97- name: Delete template with minimal options
98  community.general.proxmox_template:
99    node: uk-mc02
100    api_user: root@pam
101    api_password: 1q2w3e
102    api_host: node1
103    template: ubuntu-14.04-x86_64.tar.gz
104    state: absent
105
106- name: Download proxmox appliance container template
107  community.general.proxmox_template:
108    node: uk-mc02
109    api_user: root@pam
110    api_password: 1q2w3e
111    api_host: node1
112    storage: local
113    content_type: vztmpl
114    template: ubuntu-20.04-standard_20.04-1_amd64.tar.gz
115'''
116
117import os
118import time
119
120try:
121    from proxmoxer import ProxmoxAPI
122    HAS_PROXMOXER = True
123except ImportError:
124    HAS_PROXMOXER = False
125
126from ansible.module_utils.basic import AnsibleModule, env_fallback
127
128
129def get_template(proxmox, node, storage, content_type, template):
130    return [True for tmpl in proxmox.nodes(node).storage(storage).content.get()
131            if tmpl['volid'] == '%s:%s/%s' % (storage, content_type, template)]
132
133
134def task_status(module, proxmox, node, taskid, timeout):
135    """
136    Check the task status and wait until the task is completed or the timeout is reached.
137    """
138    while timeout:
139        task_status = proxmox.nodes(node).tasks(taskid).status.get()
140        if task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK':
141            return True
142        timeout = timeout - 1
143        if timeout == 0:
144            module.fail_json(msg='Reached timeout while waiting for uploading/downloading template. Last line in task before timeout: %s'
145                                 % proxmox.node(node).tasks(taskid).log.get()[:1])
146
147        time.sleep(1)
148    return False
149
150
151def upload_template(module, proxmox, node, storage, content_type, realpath, timeout):
152    taskid = proxmox.nodes(node).storage(storage).upload.post(content=content_type, filename=open(realpath, 'rb'))
153    return task_status(module, proxmox, node, taskid, timeout)
154
155
156def download_template(module, proxmox, node, storage, template, timeout):
157    taskid = proxmox.nodes(node).aplinfo.post(storage=storage, template=template)
158    return task_status(module, proxmox, node, taskid, timeout)
159
160
161def delete_template(module, proxmox, node, storage, content_type, template, timeout):
162    volid = '%s:%s/%s' % (storage, content_type, template)
163    proxmox.nodes(node).storage(storage).content.delete(volid)
164    while timeout:
165        if not get_template(proxmox, node, storage, content_type, template):
166            return True
167        timeout = timeout - 1
168        if timeout == 0:
169            module.fail_json(msg='Reached timeout while waiting for deleting template.')
170
171        time.sleep(1)
172    return False
173
174
175def main():
176    module = AnsibleModule(
177        argument_spec=dict(
178            api_host=dict(required=True),
179            api_password=dict(no_log=True, fallback=(env_fallback, ['PROXMOX_PASSWORD'])),
180            api_token_id=dict(no_log=True),
181            api_token_secret=dict(no_log=True),
182            api_user=dict(required=True),
183            validate_certs=dict(type='bool', default=False),
184            node=dict(),
185            src=dict(type='path'),
186            template=dict(),
187            content_type=dict(default='vztmpl', choices=['vztmpl', 'iso']),
188            storage=dict(default='local'),
189            timeout=dict(type='int', default=30),
190            force=dict(type='bool', default=False),
191            state=dict(default='present', choices=['present', 'absent']),
192        ),
193        required_together=[('api_token_id', 'api_token_secret')],
194        required_one_of=[('api_password', 'api_token_id')],
195        required_if=[('state', 'absent', ['template'])]
196    )
197
198    if not HAS_PROXMOXER:
199        module.fail_json(msg='proxmoxer required for this module')
200
201    state = module.params['state']
202    api_host = module.params['api_host']
203    api_password = module.params['api_password']
204    api_token_id = module.params['api_token_id']
205    api_token_secret = module.params['api_token_secret']
206    api_user = module.params['api_user']
207    validate_certs = module.params['validate_certs']
208    node = module.params['node']
209    storage = module.params['storage']
210    timeout = module.params['timeout']
211
212    auth_args = {'user': api_user}
213    if not (api_token_id and api_token_secret):
214        auth_args['password'] = api_password
215    else:
216        auth_args['token_name'] = api_token_id
217        auth_args['token_value'] = api_token_secret
218
219    try:
220        proxmox = ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
221        # Used to test the validity of the token if given
222        proxmox.version.get()
223    except Exception as e:
224        module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
225
226    if state == 'present':
227        try:
228            content_type = module.params['content_type']
229            src = module.params['src']
230
231            # download appliance template
232            if content_type == 'vztmpl' and not src:
233                template = module.params['template']
234
235                if not template:
236                    module.fail_json(msg='template param for downloading appliance template is mandatory')
237
238                if get_template(proxmox, node, storage, content_type, template) and not module.params['force']:
239                    module.exit_json(changed=False, msg='template with volid=%s:%s/%s already exists' % (storage, content_type, template))
240
241                if download_template(module, proxmox, node, storage, template, timeout):
242                    module.exit_json(changed=True, msg='template with volid=%s:%s/%s downloaded' % (storage, content_type, template))
243
244            template = os.path.basename(src)
245            if get_template(proxmox, node, storage, content_type, template) and not module.params['force']:
246                module.exit_json(changed=False, msg='template with volid=%s:%s/%s is already exists' % (storage, content_type, template))
247            elif not src:
248                module.fail_json(msg='src param to uploading template file is mandatory')
249            elif not (os.path.exists(src) and os.path.isfile(src)):
250                module.fail_json(msg='template file on path %s not exists' % src)
251
252            if upload_template(module, proxmox, node, storage, content_type, src, timeout):
253                module.exit_json(changed=True, msg='template with volid=%s:%s/%s uploaded' % (storage, content_type, template))
254        except Exception as e:
255            module.fail_json(msg="uploading/downloading of template %s failed with exception: %s" % (template, e))
256
257    elif state == 'absent':
258        try:
259            content_type = module.params['content_type']
260            template = module.params['template']
261
262            if not get_template(proxmox, node, storage, content_type, template):
263                module.exit_json(changed=False, msg='template with volid=%s:%s/%s is already deleted' % (storage, content_type, template))
264
265            if delete_template(module, proxmox, node, storage, content_type, template, timeout):
266                module.exit_json(changed=True, msg='template with volid=%s:%s/%s deleted' % (storage, content_type, template))
267        except Exception as e:
268            module.fail_json(msg="deleting of template %s failed with exception: %s" % (template, e))
269
270
271if __name__ == '__main__':
272    main()
273