1#!/usr/local/bin/python3.8
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
22from __future__ import (absolute_import, division, print_function)
23__metaclass__ = type
24
25DOCUMENTATION = '''
26---
27module: ovirt_external_provider
28short_description: Module to manage external providers in oVirt/RHV
29version_added: "1.0.0"
30author: "Ondra Machacek (@machacekondra)"
31description:
32    - "Module to manage external providers in oVirt/RHV"
33options:
34    name:
35        description:
36            - "Name of the external provider to manage."
37        type: str
38    state:
39        description:
40            - "Should the external be present or absent"
41            - "When you are using absent for I(os_volume), you need to make
42               sure that SD is not attached to the data center!"
43        choices: ['present', 'absent']
44        default: present
45        type: str
46    description:
47        description:
48            - "Description of the external provider."
49        type: str
50    type:
51        description:
52            - "Type of the external provider."
53        choices: ['os_image', 'network', 'os_volume', 'foreman']
54        required: true
55        type: str
56        aliases: ['provider']
57    url:
58        description:
59            - "URL where external provider is hosted."
60            - "Applicable for those types: I(os_image), I(os_volume), I(network) and I(foreman)."
61        type: str
62    username:
63        description:
64            - "Username to be used for login to external provider."
65            - "Applicable for all types."
66        type: str
67    password:
68        description:
69            - "Password of the user specified in C(username) parameter."
70            - "Applicable for all types."
71        type: str
72    tenant_name:
73        description:
74            - "Name of the tenant."
75            - "Applicable for those types: I(os_image), I(os_volume) and I(network)."
76        aliases: ['tenant']
77        type: str
78    authentication_url:
79        description:
80            - "Keystone authentication URL of the openstack provider."
81            - "Applicable for those types: I(os_image), I(os_volume) and I(network)."
82        aliases: ['auth_url']
83        type: str
84    data_center:
85        description:
86            - "Name of the data center where provider should be attached."
87            - "Applicable for those type: I(os_volume)."
88        type: str
89    read_only:
90        description:
91            - "Specify if the network should be read only."
92            - "Applicable if C(type) is I(network)."
93        type: bool
94    network_type:
95        description:
96            - "Type of the external network provider either external (for example OVN) or neutron."
97            - "Applicable if C(type) is I(network)."
98        choices: ['external', 'neutron']
99        default: 'external'
100        type: str
101    authentication_keys:
102        description:
103            - "List of authentication keys."
104            - "When you will not pass these keys and there are already some
105               of them defined in the system they will be removed."
106            - "Applicable for I(os_volume)."
107        suboptions:
108            uuid:
109                description:
110                    - The uuid which will be used.
111            value:
112                description:
113                    - The value which will be used.
114        default: []
115        type: list
116        elements: dict
117        aliases: ['auth_keys']
118extends_documentation_fragment: ovirt.ovirt.ovirt
119'''
120
121EXAMPLES = '''
122# Examples don't contain auth parameter for simplicity,
123# look at ovirt_auth module to see how to reuse authentication:
124
125# Add image external provider:
126- ovirt.ovirt.ovirt_external_provider:
127    name: image_provider
128    type: os_image
129    url: http://1.2.3.4:9292
130    username: admin
131    password: 123456
132    tenant: admin
133    auth_url: http://1.2.3.4:35357/v2.0
134
135# Add volume external provider:
136- ovirt.ovirt.ovirt_external_provider:
137    name: image_provider
138    type: os_volume
139    url: http://1.2.3.4:9292
140    username: admin
141    password: 123456
142    tenant: admin
143    auth_url: http://1.2.3.4:5000/v2.0
144    authentication_keys:
145      -
146        uuid: "1234567-a1234-12a3-a234-123abc45678"
147        value: "ABCD00000000111111222333445w=="
148
149# Add foreman provider:
150- ovirt.ovirt.ovirt_external_provider:
151    name: foreman_provider
152    type: foreman
153    url: https://foreman.example.com
154    username: admin
155    password: 123456
156
157# Add external network provider for OVN:
158- ovirt.ovirt.ovirt_external_provider:
159    name: ovn_provider
160    type: network
161    network_type: external
162    url: http://1.2.3.4:9696
163
164# Remove image external provider:
165- ovirt.ovirt.ovirt_external_provider:
166    state: absent
167    name: image_provider
168    type: os_image
169'''
170
171RETURN = '''
172id:
173    description: ID of the external provider which is managed
174    returned: On success if external provider is found.
175    type: str
176    sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c
177external_host_provider:
178    description: "Dictionary of all the external_host_provider attributes. External provider attributes can be found on your oVirt/RHV instance
179                  at following url: http://ovirt.github.io/ovirt-engine-api-model/master/#types/external_host_provider."
180    returned: "On success and if parameter 'type: foreman' is used."
181    type: dict
182openstack_image_provider:
183    description: "Dictionary of all the openstack_image_provider attributes. External provider attributes can be found on your oVirt/RHV instance
184                  at following url: http://ovirt.github.io/ovirt-engine-api-model/master/#types/openstack_image_provider."
185    returned: "On success and if parameter 'type: os_image' is used."
186    type: dict
187openstack_volume_provider:
188    description: "Dictionary of all the openstack_volume_provider attributes. External provider attributes can be found on your oVirt/RHV instance
189                  at following url: http://ovirt.github.io/ovirt-engine-api-model/master/#types/openstack_volume_provider."
190    returned: "On success and if parameter 'type: os_volume' is used."
191    type: dict
192openstack_network_provider:
193    description: "Dictionary of all the openstack_network_provider attributes. External provider attributes can be found on your oVirt/RHV instance
194                  at following url: http://ovirt.github.io/ovirt-engine-api-model/master/#types/openstack_network_provider."
195    returned: "On success and if parameter 'type: network' is used."
196    type: dict
197'''
198
199import traceback
200
201try:
202    import ovirtsdk4.types as otypes
203except ImportError:
204    pass
205
206from ansible.module_utils.basic import AnsibleModule
207from ansible_collections.ovirt.ovirt.plugins.module_utils.ovirt import (
208    BaseModule,
209    check_params,
210    check_sdk,
211    create_connection,
212    equal,
213    ovirt_full_argument_spec,
214)
215
216
217OS_VOLUME = 'os_volume'
218OS_IMAGE = 'os_image'
219NETWORK = 'network'
220FOREMAN = 'foreman'
221
222
223class ExternalProviderModule(BaseModule):
224
225    non_provider_params = ['type', 'authentication_keys', 'data_center']
226
227    def provider_type(self, provider_type):
228        self._provider_type = provider_type
229
230    def provider_module_params(self):
231        provider_params = [
232            (key, value) for key, value in self._module.params.items() if key
233            not in self.non_provider_params
234        ]
235        provider_params.append(('data_center', self.get_data_center()))
236        return provider_params
237
238    def get_data_center(self):
239        dc_name = self._module.params.get("data_center", None)
240        if dc_name:
241            system_service = self._connection.system_service()
242            data_centers_service = system_service.data_centers_service()
243            return data_centers_service.list(
244                search='name=%s' % dc_name,
245            )[0]
246        return dc_name
247
248    def build_entity(self):
249        provider_type = self._provider_type(
250            requires_authentication=self._module.params.get('username') is not None,
251        )
252        if self._module.params.pop('type') == NETWORK:
253            setattr(
254                provider_type,
255                'type',
256                otypes.OpenStackNetworkProviderType(self._module.params.pop('network_type'))
257            )
258
259        for key, value in self.provider_module_params():
260            if hasattr(provider_type, key):
261                setattr(provider_type, key, value)
262
263        return provider_type
264
265    def update_check(self, entity):
266        return (
267            equal(self._module.params.get('description'), entity.description) and
268            equal(self._module.params.get('url'), entity.url) and
269            equal(self._module.params.get('authentication_url'), entity.authentication_url) and
270            equal(self._module.params.get('tenant_name'), getattr(entity, 'tenant_name', None)) and
271            equal(self._module.params.get('username'), entity.username)
272        )
273
274    def update_volume_provider_auth_keys(
275        self, provider, providers_service, keys
276    ):
277        """
278        Update auth keys for volume provider, if not exist add them or remove
279        if they are not specified and there are already defined in the external
280        volume provider.
281
282        Args:
283            provider (dict): Volume provider details.
284            providers_service (openstack_volume_providers_service): Provider
285                service.
286            keys (list): Keys to be updated/added to volume provider, each key
287                is represented as dict with keys: uuid, value.
288        """
289
290        provider_service = providers_service.provider_service(provider['id'])
291        auth_keys_service = provider_service.authentication_keys_service()
292        provider_keys = auth_keys_service.list()
293        # removing keys which are not defined
294        for key in [
295            k.id for k in provider_keys if k.uuid not in [
296                defined_key['uuid'] for defined_key in keys
297            ]
298        ]:
299            self.changed = True
300            if not self._module.check_mode:
301                auth_keys_service.key_service(key).remove()
302        if not (provider_keys or keys):
303            # Nothing need to do when both are empty.
304            return
305        for key in keys:
306            key_id_for_update = None
307            for existing_key in provider_keys:
308                if key['uuid'] == existing_key.uuid:
309                    key_id_for_update = existing_key.id
310
311            auth_key_usage_type = (
312                otypes.OpenstackVolumeAuthenticationKeyUsageType("ceph")
313            )
314            auth_key = otypes.OpenstackVolumeAuthenticationKey(
315                usage_type=auth_key_usage_type,
316                uuid=key['uuid'],
317                value=key['value'],
318            )
319
320            if not key_id_for_update:
321                self.changed = True
322                if not self._module.check_mode:
323                    auth_keys_service.add(auth_key)
324            else:
325                # We cannot really distinguish here if it was really updated cause
326                # we cannot take key value to check if it was changed or not. So
327                # for sure we update here always.
328                self.changed = True
329                if not self._module.check_mode:
330                    auth_key_service = (
331                        auth_keys_service.key_service(key_id_for_update)
332                    )
333                    auth_key_service.update(auth_key)
334
335
336def _external_provider_service(provider_type, system_service):
337    if provider_type == OS_IMAGE:
338        return otypes.OpenStackImageProvider, system_service.openstack_image_providers_service()
339    elif provider_type == NETWORK:
340        return otypes.OpenStackNetworkProvider, system_service.openstack_network_providers_service()
341    elif provider_type == OS_VOLUME:
342        return otypes.OpenStackVolumeProvider, system_service.openstack_volume_providers_service()
343    elif provider_type == FOREMAN:
344        return otypes.ExternalHostProvider, system_service.external_host_providers_service()
345
346
347def main():
348    argument_spec = ovirt_full_argument_spec(
349        state=dict(
350            choices=['present', 'absent'],
351            default='present',
352        ),
353        name=dict(default=None),
354        description=dict(default=None),
355        type=dict(
356            required=True,
357            choices=[
358                OS_IMAGE, NETWORK, OS_VOLUME, FOREMAN,
359            ],
360            aliases=['provider'],
361        ),
362        url=dict(default=None),
363        username=dict(default=None),
364        password=dict(default=None, no_log=True),
365        tenant_name=dict(default=None, aliases=['tenant']),
366        authentication_url=dict(default=None, aliases=['auth_url']),
367        data_center=dict(default=None),
368        read_only=dict(default=None, type='bool'),
369        network_type=dict(
370            default='external',
371            choices=['external', 'neutron'],
372        ),
373        authentication_keys=dict(
374            default=[], aliases=['auth_keys'], type='list', no_log=True, elements='dict'
375        ),
376    )
377    module = AnsibleModule(
378        argument_spec=argument_spec,
379        supports_check_mode=True,
380    )
381
382    check_sdk(module)
383    check_params(module)
384
385    try:
386        auth = module.params.pop('auth')
387        connection = create_connection(auth)
388        provider_type_param = module.params.get('type')
389        provider_type, external_providers_service = _external_provider_service(
390            provider_type=provider_type_param,
391            system_service=connection.system_service(),
392        )
393        external_providers_module = ExternalProviderModule(
394            connection=connection,
395            module=module,
396            service=external_providers_service,
397        )
398        external_providers_module.provider_type(provider_type)
399
400        state = module.params.pop('state')
401        if state == 'absent':
402            ret = external_providers_module.remove()
403        elif state == 'present':
404            ret = external_providers_module.create()
405            openstack_volume_provider_id = ret.get('id')
406            if (
407                provider_type_param == OS_VOLUME and
408                openstack_volume_provider_id
409            ):
410                external_providers_module.update_volume_provider_auth_keys(
411                    ret, external_providers_service,
412                    module.params.get('authentication_keys'),
413                )
414
415        module.exit_json(**ret)
416
417    except Exception as e:
418        module.fail_json(msg=str(e), exception=traceback.format_exc())
419    finally:
420        connection.close(logout=auth.get('token') is None)
421
422
423if __name__ == "__main__":
424    main()
425