1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2016 Red Hat, Inc.
5#
6# This file is part of Ansible
7#
8# Ansible is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# Ansible is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
20#
21
22ANSIBLE_METADATA = {'metadata_version': '1.1',
23                    'status': ['preview'],
24                    'supported_by': 'community'}
25
26
27DOCUMENTATION = '''
28---
29module: ovirt_vmpool
30short_description: Module to manage VM pools in oVirt/RHV
31version_added: "2.3"
32author: "Ondra Machacek (@machacekondra)"
33description:
34    - "Module to manage VM pools in oVirt/RHV."
35options:
36    id:
37        description:
38            - "ID of the vmpool to manage."
39        version_added: "2.8"
40    name:
41        description:
42            - "Name of the VM pool to manage."
43        required: true
44    comment:
45        description:
46            - Comment of the Virtual Machine pool.
47    state:
48        description:
49            - "Should the VM pool be present/absent."
50            - "Note that when C(state) is I(absent) all VMs in VM pool are stopped and removed."
51        choices: ['present', 'absent']
52        default: present
53    template:
54        description:
55            - "Name of the template, which will be used to create VM pool."
56    description:
57        description:
58            - "Description of the VM pool."
59    cluster:
60        description:
61            - "Name of the cluster, where VM pool should be created."
62    type:
63        description:
64            - "Type of the VM pool. Either manual or automatic."
65            - "C(manual) - The administrator is responsible for explicitly returning the virtual machine to the pool.
66               The virtual machine reverts to the original base image after the administrator returns it to the pool."
67            - "C(Automatic) - When the virtual machine is shut down, it automatically reverts to its base image and
68               is returned to the virtual machine pool."
69            - "Default value is set by engine."
70        choices: ['manual', 'automatic']
71    vm_per_user:
72        description:
73            - "Maximum number of VMs a single user can attach to from this pool."
74            - "Default value is set by engine."
75    prestarted:
76        description:
77            - "Number of pre-started VMs defines the number of VMs in run state, that are waiting
78               to be attached to Users."
79            - "Default value is set by engine."
80    vm_count:
81        description:
82            - "Number of VMs in the pool."
83            - "Default value is set by engine."
84    vm:
85        description:
86            - "For creating vm pool without editing template."
87            - "Note: You can use C(vm) only for creating vm pool."
88        type: dict
89        suboptions:
90            comment:
91                description:
92                    - Comment of the Virtual Machine.
93            timezone:
94                description:
95                    - Sets time zone offset of the guest hardware clock.
96                    - For example C(Etc/GMT)
97            memory:
98                description:
99                    - Amount of memory of the Virtual Machine. Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB).
100                    - Default value is set by engine.
101            memory_guaranteed:
102                description:
103                    - Amount of minimal guaranteed memory of the Virtual Machine.
104                      Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB).
105                    - C(memory_guaranteed) parameter can't be lower than C(memory) parameter.
106                    - Default value is set by engine.
107            memory_max:
108                description:
109                    - Upper bound of virtual machine memory up to which memory hot-plug can be performed.
110                      Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB).
111                    - Default value is set by engine.
112            cloud_init:
113                description:
114                    - Dictionary with values for Unix-like Virtual Machine initialization using cloud init.
115                    - C(host_name) - Hostname to be set to Virtual Machine when deployed.
116                    - C(timezone) - Timezone to be set to Virtual Machine when deployed.
117                    - C(user_name) - Username to be used to set password to Virtual Machine when deployed.
118                    - C(root_password) - Password to be set for user specified by C(user_name) parameter.
119                    - C(authorized_ssh_keys) - Use this SSH keys to login to Virtual Machine.
120                    - C(regenerate_ssh_keys) - If I(True) SSH keys will be regenerated on Virtual Machine.
121                    - C(custom_script) - Cloud-init script which will be executed on Virtual Machine when deployed.  This is appended to the end of the
122                      cloud-init script generated by any other options.
123                    - C(dns_servers) - DNS servers to be configured on Virtual Machine.
124                    - C(dns_search) - DNS search domains to be configured on Virtual Machine.
125                    - C(nic_boot_protocol) - Set boot protocol of the network interface of Virtual Machine. Can be one of C(none), C(dhcp) or C(static).
126                    - C(nic_ip_address) - If boot protocol is static, set this IP address to network interface of Virtual Machine.
127                    - C(nic_netmask) - If boot protocol is static, set this netmask to network interface of Virtual Machine.
128                    - C(nic_gateway) - If boot protocol is static, set this gateway to network interface of Virtual Machine.
129                    - C(nic_name) - Set name to network interface of Virtual Machine.
130                    - C(nic_on_boot) - If I(True) network interface will be set to start on boot.
131            sso:
132                description:
133                    - "I(True) enable Single Sign On by Guest Agent, I(False) to disable it. By default is chosen by oVirt/RHV engine."
134                type: bool
135            smartcard_enabled:
136                description:
137                    - "If I(true), use smart card authentication."
138                type: bool
139            nics:
140                description:
141                    - List of NICs, which should be attached to Virtual Machine. NIC is described by following dictionary.
142                    - C(name) - Name of the NIC.
143                    - C(profile_name) - Profile name where NIC should be attached.
144                    - C(interface) -  Type of the network interface. One of following I(virtio), I(e1000), I(rtl8139), default is I(virtio).
145                    - C(mac_address) - Custom MAC address of the network interface, by default it's obtained from MAC pool.
146                    - NOTE - This parameter is used only when C(state) is I(running) or I(present) and is able to only create NICs.
147                    - To manage NICs of the VM in more depth please use M(ovirt_nics) module instead.
148        version_added: "2.9"
149extends_documentation_fragment: ovirt
150'''
151
152EXAMPLES = '''
153# Examples don't contain auth parameter for simplicity,
154# look at ovirt_auth module to see how to reuse authentication:
155
156- name: Create VM pool from template
157  ovirt_vmpool:
158    cluster: mycluster
159    name: myvmpool
160    template: rhel7
161    vm_count: 2
162    prestarted: 2
163    vm_per_user: 1
164
165- name: Remove vmpool, note that all VMs in pool will be stopped and removed
166  ovirt_vmpool:
167    state: absent
168    name: myvmpool
169
170- name: Change Pool Name
171  ovirt_vmpool:
172    id: 00000000-0000-0000-0000-000000000000
173    name: "new_pool_name"
174
175- name: Create vm pool and override the pool values
176  ovirt_vmpool:
177    cluster: mycluster
178    name: vmpool
179    template: blank
180    vm_count: 2
181    prestarted: 1
182    vm_per_user: 1
183    vm:
184      memory: 4GiB
185      memory_guaranteed: 4GiB
186      memory_max: 10GiB
187      comment: vncomment
188      cloud_init:
189        nic_boot_protocol: static
190        nic_ip_address: 10.34.60.86
191        nic_netmask: 255.255.252.0
192        nic_gateway: 10.34.63.254
193        nic_name: eth1
194        nic_on_boot: true
195        host_name: example.com
196        custom_script: |
197          write_files:
198           - content: |
199               Hello, world!
200             path: /tmp/greeting.txt
201             permissions: '0644'
202        user_name: root
203        root_password: super_password
204      nics:
205        - name: nicname
206          interface: virtio
207          profile_name: network
208
209'''
210
211RETURN = '''
212id:
213    description: ID of the VM pool which is managed
214    returned: On success if VM pool is found.
215    type: str
216    sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c
217vm_pool:
218    description: "Dictionary of all the VM pool attributes. VM pool attributes can be found on your oVirt/RHV instance
219                  at following url: http://ovirt.github.io/ovirt-engine-api-model/master/#types/vm_pool."
220    returned: On success if VM pool is found.
221    type: dict
222'''
223
224try:
225    import ovirtsdk4.types as otypes
226except ImportError:
227    pass
228
229import traceback
230
231from ansible.module_utils.basic import AnsibleModule
232from ansible.module_utils.ovirt import (
233    BaseModule,
234    check_params,
235    check_sdk,
236    create_connection,
237    equal,
238    get_link_name,
239    ovirt_full_argument_spec,
240    wait,
241    convert_to_bytes,
242    search_by_name,
243)
244
245
246class VmPoolsModule(BaseModule):
247    def __init__(self, *args, **kwargs):
248        super(VmPoolsModule, self).__init__(*args, **kwargs)
249        self._initialization = None
250
251    def build_entity(self):
252        vm = self.param('vm')
253        return otypes.VmPool(
254            id=self._module.params['id'],
255            name=self._module.params['name'],
256            description=self._module.params['description'],
257            comment=self._module.params['comment'],
258            cluster=otypes.Cluster(
259                name=self._module.params['cluster']
260            ) if self._module.params['cluster'] else None,
261            template=otypes.Template(
262                name=self._module.params['template']
263            ) if self._module.params['template'] else None,
264            max_user_vms=self._module.params['vm_per_user'],
265            prestarted_vms=self._module.params['prestarted'],
266            size=self._module.params['vm_count'],
267            type=otypes.VmPoolType(
268                self._module.params['type']
269            ) if self._module.params['type'] else None,
270            vm=self.build_vm(vm) if self._module.params['vm'] else None,
271        )
272
273    def build_vm(self, vm):
274        return otypes.Vm(
275            comment=vm.get('comment'),
276            memory=convert_to_bytes(
277                vm.get('memory')
278            ) if vm.get('memory') else None,
279            memory_policy=otypes.MemoryPolicy(
280                guaranteed=convert_to_bytes(vm.get('memory_guaranteed')),
281                max=convert_to_bytes(vm.get('memory_max')),
282            ) if any((
283                vm.get('memory_guaranteed'),
284                vm.get('memory_max')
285            )) else None,
286            initialization=self.get_initialization(vm),
287            display=otypes.Display(
288                smartcard_enabled=vm.get('smartcard_enabled')
289            ) if vm.get('smartcard_enabled') is not None else None,
290            sso=(
291                otypes.Sso(
292                    methods=[otypes.Method(id=otypes.SsoMethod.GUEST_AGENT)] if vm.get('sso') else []
293                )
294            ) if vm.get('sso') is not None else None,
295            time_zone=otypes.TimeZone(
296                name=vm.get('timezone'),
297            ) if vm.get('timezone') else None,
298        )
299
300    def get_initialization(self, vm):
301        if self._initialization is not None:
302            return self._initialization
303
304        sysprep = vm.get('sysprep')
305        cloud_init = vm.get('cloud_init')
306        cloud_init_nics = vm.get('cloud_init_nics') or []
307        if cloud_init is not None:
308            cloud_init_nics.append(cloud_init)
309
310        if cloud_init or cloud_init_nics:
311            self._initialization = otypes.Initialization(
312                nic_configurations=[
313                    otypes.NicConfiguration(
314                        boot_protocol=otypes.BootProtocol(
315                            nic.pop('nic_boot_protocol').lower()
316                        ) if nic.get('nic_boot_protocol') else None,
317                        name=nic.pop('nic_name', None),
318                        on_boot=nic.pop('nic_on_boot', None),
319                        ip=otypes.Ip(
320                            address=nic.pop('nic_ip_address', None),
321                            netmask=nic.pop('nic_netmask', None),
322                            gateway=nic.pop('nic_gateway', None),
323                        ) if (
324                            nic.get('nic_gateway') is not None or
325                            nic.get('nic_netmask') is not None or
326                            nic.get('nic_ip_address') is not None
327                        ) else None,
328                    )
329                    for nic in cloud_init_nics
330                    if (
331                        nic.get('nic_gateway') is not None or
332                        nic.get('nic_netmask') is not None or
333                        nic.get('nic_ip_address') is not None or
334                        nic.get('nic_boot_protocol') is not None or
335                        nic.get('nic_on_boot') is not None
336                    )
337                ] if cloud_init_nics else None,
338                **cloud_init
339            )
340        elif sysprep:
341            self._initialization = otypes.Initialization(
342                **sysprep
343            )
344        return self._initialization
345
346    def get_vms(self, entity):
347        vms = self._connection.system_service().vms_service().list()
348        resp = []
349        for vm in vms:
350            if vm.vm_pool is not None and vm.vm_pool.id == entity.id:
351                resp.append(vm)
352        return resp
353
354    def post_create(self, entity):
355        vm_param = self.param('vm')
356        if vm_param is not None and vm_param.get('nics') is not None:
357            vms = self.get_vms(entity)
358            for vm in vms:
359                self.__attach_nics(vm, vm_param)
360
361    def __attach_nics(self, entity, vm_param):
362        # Attach NICs to VM, if specified:
363        vms_service = self._connection.system_service().vms_service()
364        nics_service = vms_service.service(entity.id).nics_service()
365        for nic in vm_param.get('nics'):
366            if search_by_name(nics_service, nic.get('name')) is None:
367                if not self._module.check_mode:
368                    nics_service.add(
369                        otypes.Nic(
370                            name=nic.get('name'),
371                            interface=otypes.NicInterface(
372                                nic.get('interface', 'virtio')
373                            ),
374                            vnic_profile=otypes.VnicProfile(
375                                id=self.__get_vnic_profile_id(nic),
376                            ) if nic.get('profile_name') else None,
377                            mac=otypes.Mac(
378                                address=nic.get('mac_address')
379                            ) if nic.get('mac_address') else None,
380                        )
381                    )
382                self.changed = True
383
384    def __get_vnic_profile_id(self, nic):
385        """
386        Return VNIC profile ID looked up by it's name, because there can be
387        more VNIC profiles with same name, other criteria of filter is cluster.
388        """
389        vnics_service = self._connection.system_service().vnic_profiles_service()
390        clusters_service = self._connection.system_service().clusters_service()
391        cluster = search_by_name(clusters_service, self.param('cluster'))
392        profiles = [
393            profile for profile in vnics_service.list()
394            if profile.name == nic.get('profile_name')
395        ]
396        cluster_networks = [
397            net.id for net in self._connection.follow_link(cluster.networks)
398        ]
399        try:
400            return next(
401                profile.id for profile in profiles
402                if profile.network.id in cluster_networks
403            )
404        except StopIteration:
405            raise Exception(
406                "Profile '%s' was not found in cluster '%s'" % (
407                    nic.get('profile_name'),
408                    self.param('cluster')
409                )
410            )
411
412    def update_check(self, entity):
413        return (
414            equal(self._module.params.get('name'), entity.name) and
415            equal(self._module.params.get('cluster'), get_link_name(self._connection, entity.cluster)) and
416            equal(self._module.params.get('description'), entity.description) and
417            equal(self._module.params.get('comment'), entity.comment) and
418            equal(self._module.params.get('vm_per_user'), entity.max_user_vms) and
419            equal(self._module.params.get('prestarted'), entity.prestarted_vms) and
420            equal(self._module.params.get('vm_count'), entity.size)
421        )
422
423
424def main():
425    argument_spec = ovirt_full_argument_spec(
426        id=dict(default=None),
427        state=dict(
428            choices=['present', 'absent'],
429            default='present',
430        ),
431        name=dict(required=True),
432        template=dict(default=None),
433        cluster=dict(default=None),
434        description=dict(default=None),
435        vm=dict(default=None, type='dict'),
436        comment=dict(default=None),
437        vm_per_user=dict(default=None, type='int'),
438        prestarted=dict(default=None, type='int'),
439        vm_count=dict(default=None, type='int'),
440        type=dict(default=None, choices=['automatic', 'manual']),
441    )
442    module = AnsibleModule(
443        argument_spec=argument_spec,
444        supports_check_mode=True,
445    )
446
447    check_sdk(module)
448    check_params(module)
449
450    try:
451        auth = module.params.pop('auth')
452        connection = create_connection(auth)
453        vm_pools_service = connection.system_service().vm_pools_service()
454        vm_pools_module = VmPoolsModule(
455            connection=connection,
456            module=module,
457            service=vm_pools_service,
458        )
459
460        state = module.params['state']
461        if state == 'present':
462            ret = vm_pools_module.create()
463
464            # Wait for all VM pool VMs to be created:
465            if module.params['wait']:
466                vms_service = connection.system_service().vms_service()
467                for vm in vms_service.list(search='pool=%s' % module.params['name']):
468                    wait(
469                        service=vms_service.service(vm.id),
470                        condition=lambda vm: vm.status in [otypes.VmStatus.DOWN, otypes.VmStatus.UP],
471                        timeout=module.params['timeout'],
472                    )
473
474        elif state == 'absent':
475            ret = vm_pools_module.remove()
476
477        module.exit_json(**ret)
478    except Exception as e:
479        module.fail_json(msg=str(e), exception=traceback.format_exc())
480    finally:
481        connection.close(logout=auth.get('token') is None)
482
483
484if __name__ == "__main__":
485    main()
486