diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 685a1d0f53..430617179f 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -440,6 +440,8 @@ files: maintainers: claco $modules/cloud/scaleway/: maintainers: $team_scaleway + $modules/cloud/scaleway/scaleway_compute_private_network.py: + maintainers: pastral $modules/cloud/scaleway/scaleway_database_backup.py: maintainers: guillaume_ro_fr $modules/cloud/scaleway/scaleway_image_info.py: diff --git a/meta/runtime.yml b/meta/runtime.yml index e15dafd653..14391a7e71 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1357,6 +1357,8 @@ plugin_routing: redirect: community.general.notification.say scaleway_compute: redirect: community.general.cloud.scaleway.scaleway_compute + scaleway_compute_private_network: + redirect: community.general.cloud.scaleway.scaleway_compute_private_network scaleway_database_backup: redirect: community.general.cloud.scaleway.scaleway_database_backup scaleway_image_facts: diff --git a/plugins/modules/cloud/scaleway/scaleway_compute_private_network.py b/plugins/modules/cloud/scaleway/scaleway_compute_private_network.py new file mode 100644 index 0000000000..2e23b324cd --- /dev/null +++ b/plugins/modules/cloud/scaleway/scaleway_compute_private_network.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Scaleway VPC management module +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: scaleway_compute_private_network +short_description: Scaleway compute - private network management +version_added: 5.2.0 +author: Pascal MANGIN (@pastral) +description: + - This module add or remove a private network to a compute instance + (U(https://developer.scaleway.com)). +extends_documentation_fragment: +- community.general.scaleway + + +options: + state: + type: str + description: + - Indicate desired state of the VPC. + default: present + choices: + - present + - absent + + project: + type: str + description: + - Project identifier. + required: true + + region: + type: str + description: + - Scaleway region to use (for example C(par1)). + required: true + choices: + - ams1 + - EMEA-NL-EVS + - par1 + - EMEA-FR-PAR1 + - par2 + - EMEA-FR-PAR2 + - waw1 + - EMEA-PL-WAW1 + + compute_id: + type: str + description: + - ID of the compute instance (see M(community.general.scaleway_compute)). + required: true + + private_network_id: + type: str + description: + - ID of the private network (see M(community.general.scaleway_private_network)). + required: true + +''' + +EXAMPLES = ''' +- name: Plug a VM to a private network + community.general.scaleway_compute_private_network: + project: '{{ scw_project }}' + state: present + region: par1 + compute_id: "12345678-f1e6-40ec-83e5-12345d67ed89" + private_network_id: "22345678-f1e6-40ec-83e5-12345d67ed89" + register: nicsvpc_creation_task + +- name: Unplug a VM from a private network + community.general.scaleway_compute_private_network: + project: '{{ scw_project }}' + state: absent + region: par1 + compute_id: "12345678-f1e6-40ec-83e5-12345d67ed89" + private_network_id: "22345678-f1e6-40ec-83e5-12345d67ed89" + +''' + +RETURN = ''' +scaleway_compute_private_network: + description: Information on the VPC. + returned: success when C(state=present) + type: dict + sample: + { + "created_at": "2022-01-15T11:11:12.676445Z", + "id": "12345678-f1e6-40ec-83e5-12345d67ed89", + "name": "network", + "organization_id": "a123b4cd-ef5g-678h-90i1-jk2345678l90", + "project_id": "a123b4cd-ef5g-678h-90i1-jk2345678l90", + "tags": [ + "tag1", + "tag2", + "tag3", + "tag4", + "tag5" + ], + "updated_at": "2022-01-15T11:12:04.624837Z", + "zone": "fr-par-2" + } +''' +from ansible_collections.community.general.plugins.module_utils.scaleway import SCALEWAY_LOCATION, scaleway_argument_spec, Scaleway +from ansible.module_utils.basic import AnsibleModule + + +def get_nics_info(api, compute_id, private_network_id): + + response = api.get('servers/' + compute_id + '/private_nics') + if not response.ok: + msg = "Error during get servers information: %s: '%s' (%s)" % (response.info['msg'], response.json['message'], response.json) + api.module.fail_json(msg=msg) + + i = 0 + list_nics = response.json['private_nics'] + + while i < len(list_nics): + if list_nics[i]['private_network_id'] == private_network_id: + return list_nics[i] + i += 1 + + return None + + +def present_strategy(api, compute_id, private_network_id): + + changed = False + nic = get_nics_info(api, compute_id, private_network_id) + if nic is not None: + return changed, nic + + data = {"private_network_id": private_network_id} + changed = True + if api.module.check_mode: + return changed, {"status": "a private network would be add to a server"} + + response = api.post(path='servers/' + compute_id + '/private_nics', data=data) + + if not response.ok: + api.module.fail_json(msg='Error when adding a private network to a server [{0}: {1}]'.format(response.status_code, response.json)) + + return changed, response.json + + +def absent_strategy(api, compute_id, private_network_id): + + changed = False + nic = get_nics_info(api, compute_id, private_network_id) + if nic is None: + return changed, {} + + changed = True + if api.module.check_mode: + return changed, {"status": "private network would be destroyed"} + + response = api.delete('servers/' + compute_id + '/private_nics/' + nic['id']) + + if not response.ok: + api.module.fail_json(msg='Error deleting private network from server [{0}: {1}]'.format( + response.status_code, response.json)) + + return changed, response.json + + +def core(module): + + compute_id = module.params['compute_id'] + pn_id = module.params['private_network_id'] + + region = module.params["region"] + module.params['api_url'] = SCALEWAY_LOCATION[region]["api_endpoint"] + + api = Scaleway(module=module) + if module.params["state"] == "absent": + changed, summary = absent_strategy(api=api, compute_id=compute_id, private_network_id=pn_id) + else: + changed, summary = present_strategy(api=api, compute_id=compute_id, private_network_id=pn_id) + module.exit_json(changed=changed, scaleway_compute_private_network=summary) + + +def main(): + argument_spec = scaleway_argument_spec() + argument_spec.update(dict( + state=dict(default='present', choices=['absent', 'present']), + project=dict(required=True), + region=dict(required=True, choices=list(SCALEWAY_LOCATION.keys())), + compute_id=dict(required=True), + private_network_id=dict(required=True) + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + core(module) + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/modules/cloud/scaleway/test_scaleway_compute_private_network.py b/tests/unit/plugins/modules/cloud/scaleway/test_scaleway_compute_private_network.py new file mode 100644 index 0000000000..f474084576 --- /dev/null +++ b/tests/unit/plugins/modules/cloud/scaleway/test_scaleway_compute_private_network.py @@ -0,0 +1,179 @@ +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +import pytest + + +from ansible_collections.community.general.plugins.modules.cloud.scaleway import scaleway_compute_private_network +from ansible_collections.community.general.plugins.module_utils.scaleway import Scaleway, Response +from ansible_collections.community.general.tests.unit.plugins.modules.utils import set_module_args +from ansible_collections.community.general.tests.unit.compat.mock import patch + + +def response_without_nics(): + info = {"status": 200, + "body": '{ "private_nics": []}' + } + return Response(None, info) + + +def response_with_nics(): + info = {"status": 200, + "body": ('{ "private_nics": [{' + '"id": "c123b4cd-ef5g-678h-90i1-jk2345678l90",' + '"private_network_id": "b589b4cd-ef5g-678h-90i1-jk2345678l90",' + '"server_id": "c004b4cd-ef5g-678h-90i1-jk2345678l90",' + '"mac_address": "02:00:00:00:12:23",' + '"state": "available",' + '"creation_date": "2022-03-30T06:25:28.155973+00:00",' + '"modification_date": "2022-03-30T06:25:28.155973+00:00",' + '"zone": "fr-par-1"' + '}]}' + ) + } + return Response(None, info) + + +def response_when_add_nics(): + info = {"status": 200, + "body": ('{ "private_nics": {' + '"id": "c123b4cd-ef5g-678h-90i1-jk2345678l90",' + '"private_network_id": "b589b4cd-ef5g-678h-90i1-jk2345678l90",' + '"server_id": "c004b4cd-ef5g-678h-90i1-jk2345678l90",' + '"mac_address": "02:00:00:00:12:23",' + '"state": "available",' + '"creation_date": "2022-03-30T06:25:28.155973+00:00",' + '"modification_date": "2022-03-30T06:25:28.155973+00:00",' + '"zone": "fr-par-1"' + '}}' + ) + } + return Response(None, info) + + +def response_remove_nics(): + info = {"status": 200} + return Response(None, info) + + +def test_scaleway_private_network_without_arguments(capfd): + set_module_args({}) + with pytest.raises(SystemExit) as results: + scaleway_compute_private_network.main() + out, err = capfd.readouterr() + + assert not err + assert json.loads(out)['failed'] + + +def test_scaleway_add_nic(capfd): + os.environ['SCW_API_TOKEN'] = 'notrealtoken' + pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90' + cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90' + url = 'servers/' + cid + '/private_nics' + + set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90", + "state": "present", + "region": "par1", + "compute_id": cid, + "private_network_id": pnid + }) + + with patch.object(Scaleway, 'get') as mock_scw_get: + mock_scw_get.return_value = response_without_nics() + with patch.object(Scaleway, 'post') as mock_scw_post: + mock_scw_post.return_value = response_when_add_nics() + with pytest.raises(SystemExit) as results: + scaleway_compute_private_network.main() + mock_scw_post.assert_any_call(path=url, data={"private_network_id": pnid}) + mock_scw_get.assert_any_call(url) + + out, err = capfd.readouterr() + del os.environ['SCW_API_TOKEN'] + assert not err + assert json.loads(out)['changed'] + + +def test_scaleway_add_existing_nic(capfd): + os.environ['SCW_API_TOKEN'] = 'notrealtoken' + pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90' + cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90' + url = 'servers/' + cid + '/private_nics' + + set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90", + "state": "present", + "region": "par1", + "compute_id": cid, + "private_network_id": pnid + }) + + with patch.object(Scaleway, 'get') as mock_scw_get: + mock_scw_get.return_value = response_with_nics() + with pytest.raises(SystemExit) as results: + scaleway_compute_private_network.main() + mock_scw_get.assert_any_call(url) + + out, err = capfd.readouterr() + del os.environ['SCW_API_TOKEN'] + assert not err + assert not json.loads(out)['changed'] + + +def test_scaleway_remove_existing_nic(capfd): + os.environ['SCW_API_TOKEN'] = 'notrealtoken' + pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90' + cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90' + nicid = 'c123b4cd-ef5g-678h-90i1-jk2345678l90' + url = 'servers/' + cid + '/private_nics' + urlremove = 'servers/' + cid + '/private_nics/' + nicid + + set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90", + "state": "absent", + "region": "par1", + "compute_id": cid, + "private_network_id": pnid + }) + + with patch.object(Scaleway, 'get') as mock_scw_get: + mock_scw_get.return_value = response_with_nics() + with patch.object(Scaleway, 'delete') as mock_scw_delete: + mock_scw_delete.return_value = response_remove_nics() + with pytest.raises(SystemExit) as results: + scaleway_compute_private_network.main() + mock_scw_delete.assert_any_call(urlremove) + mock_scw_get.assert_any_call(url) + + out, err = capfd.readouterr() + + del os.environ['SCW_API_TOKEN'] + assert not err + assert json.loads(out)['changed'] + + +def test_scaleway_remove_absent_nic(capfd): + os.environ['SCW_API_TOKEN'] = 'notrealtoken' + pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90' + cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90' + url = 'servers/' + cid + '/private_nics' + + set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90", + "state": "absent", + "region": "par1", + "compute_id": cid, + "private_network_id": pnid + }) + + with patch.object(Scaleway, 'get') as mock_scw_get: + mock_scw_get.return_value = response_without_nics() + with pytest.raises(SystemExit) as results: + scaleway_compute_private_network.main() + mock_scw_get.assert_any_call(url) + + out, err = capfd.readouterr() + del os.environ['SCW_API_TOKEN'] + assert not err + assert not json.loads(out)['changed']