├── CHANGELOG.md ├── docs ├── source │ ├── modules │ │ └── .git_keep │ ├── index.rst │ ├── conf.py │ └── modules.rst └── templates │ └── module.rst.j2 ├── plugins ├── doc_fragments │ ├── __init__.py │ └── cluster_instance.py ├── module_utils │ ├── __init__.py │ ├── cluster_instance.py │ ├── auth.py │ ├── form.py │ ├── state.py │ ├── arguments.py │ ├── dns_record.py │ ├── task.py │ ├── tag.py │ ├── disk.py │ ├── errors.py │ ├── space.py │ ├── fabric.py │ ├── user.py │ ├── utils.py │ ├── vmhost.py │ ├── rest_client.py │ └── vlan.py ├── README.md └── modules │ ├── boot_sources_info.py │ ├── dns_domain_info.py │ ├── tag_info.py │ ├── dns_record_info.py │ ├── subnet_ip_range_info.py │ ├── fabric_info.py │ ├── user_info.py │ ├── subnet_info.py │ ├── space_info.py │ ├── vm_host_info.py │ ├── vlan_info.py │ ├── user.py │ ├── network_interface_info.py │ └── block_device_info.py ├── meta └── runtime.yml ├── inventory ├── tests ├── integration │ ├── targets │ │ ├── inventory │ │ │ ├── maas_inventory_no_status.yml │ │ │ ├── common │ │ │ │ ├── run_no_status_test.yml │ │ │ │ ├── cleanup.yml │ │ │ │ └── prepare.yml │ │ │ ├── tests │ │ │ │ └── test_status_no.yml │ │ │ └── runme.sh │ │ ├── machine_info │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── space │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── dns_record │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── fabric │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── user │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── vm_host_machine │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── dns_domain │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── aaa_vm_host_pre_tasks │ │ │ └── tasks │ │ │ │ └── main.yaml │ │ ├── machine │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── zzz_vm_host_post_tasks │ │ │ └── tasks │ │ │ │ └── main.yml │ │ └── vlan │ │ │ └── tasks │ │ │ └── main.yml │ └── integration_config.yml.template └── unit │ └── plugins │ ├── module_utils │ ├── test_space.py │ ├── test_fabric.py │ ├── test_disk.py │ ├── test_vmhost.py │ ├── test_partition.py │ └── test_tag.py │ ├── modules │ ├── test_space.py │ ├── test_fabric.py │ └── test_vlan.py │ └── conftest.py ├── .ansible-lint ├── CONTRIBUTING.md ├── examples ├── maas_inventory.yml ├── get_tags.yml ├── get_hosts.yml ├── get_users.yml ├── get_machines.yml ├── get_network_interface.yml ├── network_interface_link.yml ├── user.yml ├── vm_host_machine.yml ├── network_interface_physical.yml └── tag.yml ├── .github ├── workflows │ ├── cla-check.yml │ ├── stale-cron.yaml │ ├── ci-test.yml │ └── delabeler.yml └── pull_request_template.md ├── ansible.cfg ├── .gitignore ├── pyproject.toml ├── Makefile ├── README.md ├── galaxy.yml └── tox.ini /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/modules/.git_keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/doc_fragments/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/module_utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | requires_ansible: ">=2.14.0" 3 | -------------------------------------------------------------------------------- /inventory: -------------------------------------------------------------------------------- 1 | [targets] 2 | localhost ansible_connection=local -------------------------------------------------------------------------------- /tests/integration/targets/inventory/maas_inventory_no_status.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: maas.maas.inventory 3 | -------------------------------------------------------------------------------- /.ansible-lint: -------------------------------------------------------------------------------- 1 | skip_list: # or 'skip_list' to silence them completely 2 | - unnamed-task # All tasks should be named 3 | 4 | exclude_paths: 5 | - .cache 6 | - .github 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By default, any contribution to this project is made under the [GNU GPLv3 2 | license](LICENSE). 3 | 4 | The author of a change remains the copyright holder of their code (no copyright 5 | assignment). 6 | -------------------------------------------------------------------------------- /examples/maas_inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: maas.maas.inventory 3 | 4 | # Can filter inventory based on machine status. 5 | # If status is set, then only machines with that specific status will be included. 6 | status: ready 7 | -------------------------------------------------------------------------------- /.github/workflows/cla-check.yml: -------------------------------------------------------------------------------- 1 | name: cla-check 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | cla-check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check if CLA signed 10 | uses: canonical/has-signed-canonical-cla@v2 11 | -------------------------------------------------------------------------------- /.github/workflows/stale-cron.yaml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | uses: canonical/maas-github-workflows/.github/workflows/stale-cron.yaml@v0 9 | secrets: inherit 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Please include a summary of the change and which issue is fixed. 2 | 3 | Fixes #(issue-id) 4 | 5 | 6 | ## Checklist before merging 7 | - [ ] Formatting: `tox -e format` 8 | - [ ] Linting: `tox -e sanity` 9 | - [ ] Unit tests: `tox -e units` 10 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | # This ansible.cfg is used only for local testing during development. 2 | # It should not be included into final collection. 3 | [defaults] 4 | retry_files_enabled = False 5 | forks = 16 6 | collections_path = ../../../ 7 | stdout_callback = community.general.yaml 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .tox 3 | /tests/output 4 | 5 | # integration_config.yml contains connection info for the 6 | # developer instance, including credentials 7 | /tests/integration/integration_config.yml 8 | /tests/integration/inventory 9 | /docs/build 10 | /docs/source/modules/*.rst 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | 4 | [tool.isort] 5 | force_sort_within_sections = true 6 | from_first = false 7 | known_first_party = "ansible_collections.maas.maas" 8 | line_length = 79 9 | profile = "black" 10 | 11 | [tool.flake8] 12 | exclude = ["tests/output/"] 13 | ignore = ["E203", "E266", "E402", "E501", "W503", "W504"] 14 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Canonical MAAS Ansible collection 2 | ============================================ 3 | 4 | Official Ansible collection for Canonical MAAS v2 API. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Canonical MAAS Ansible collection 9 | 10 | quickstart 11 | installation 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | :caption: References 17 | 18 | roles 19 | modules 20 | -------------------------------------------------------------------------------- /tests/integration/targets/machine_info/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | - name: List machines 11 | maas.maas.machine_info: 12 | register: machines 13 | 14 | # - debug: 15 | # var: machines 16 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | project = "MAAS Ansible Collection" 2 | copyright = "2022, XLAB Steampunk" 3 | author = "XLAB Steampunk" 4 | 5 | extensions = [ 6 | "sphinx_rtd_theme", 7 | ] 8 | exclude_patterns = [] 9 | 10 | html_theme = "sphinx_rtd_theme" 11 | html_context = { 12 | "display_github": False, 13 | # "github_user": "", 14 | # "github_repo": "", 15 | # "github_version": "master", 16 | "conf_py_path": "/docs/source/", 17 | } 18 | -------------------------------------------------------------------------------- /examples/get_tags.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get list of all tags 3 | hosts: localhost 4 | tasks: 5 | - name: List tags 6 | maas.maas.tag_info: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | register: maas_tags 13 | 14 | - ansible.builtin.debug: 15 | var: maas_tags 16 | -------------------------------------------------------------------------------- /examples/get_hosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test query machines 3 | hosts: localhost 4 | tasks: 5 | - name: List machines 6 | maas.maas.vm_host_info: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | register: machines 13 | 14 | - ansible.builtin.debug: 15 | var: machines 16 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory/common/run_no_status_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Run MAAS inventory tests 3 | hosts: localhost 4 | gather_facts: false 5 | environment: 6 | MAAS_HOST: "{{ host }}" 7 | MAAS_TOKEN_KEY: "{{ token_key }}" 8 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 9 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 10 | 11 | tasks: 12 | - block: 13 | - ansible.builtin.include_tasks: 14 | file: "{{ item }}" 15 | with_fileglob: 16 | - "../tests/*_no.yml" 17 | -------------------------------------------------------------------------------- /examples/get_users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get all users 3 | hosts: localhost 4 | tasks: 5 | - name: List users 6 | maas.maas.user_info: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | name: John # Use this field to filter results by name. 13 | register: users 14 | 15 | - ansible.builtin.debug: 16 | var: users 17 | -------------------------------------------------------------------------------- /examples/get_machines.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test query machines 3 | hosts: localhost 4 | tasks: 5 | - name: List machines 6 | maas.maas.machine_info: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | fqdn: inventory-test-1.maas # Use this field to filter results by name. 13 | register: machines 14 | 15 | - ansible.builtin.debug: 16 | var: machines 17 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory/common/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup test environment 3 | hosts: localhost 4 | gather_facts: false 5 | environment: 6 | MAAS_HOST: "{{ host }}" 7 | MAAS_TOKEN_KEY: "{{ token_key }}" 8 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 9 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 10 | 11 | tasks: 12 | - name: Delete machine 13 | maas.maas.instance: 14 | fqdn: "{{ item }}" 15 | state: absent 16 | loop: 17 | - "inventory-test-1.{{ test_domain }}" 18 | - "inventory-test-2.{{ test_domain }}" 19 | -------------------------------------------------------------------------------- /examples/get_network_interface.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get list of all nics from a machine 3 | hosts: localhost 4 | tasks: 5 | - name: List nics from instance machine 6 | maas.maas.network_interface_info: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | machine: instance.maas 13 | # mac_address: 00:16:3e:46:25:e3 # Use this field to get specific nic on a machine 14 | register: nic_info 15 | - ansible.builtin.debug: 16 | var: nic_info 17 | -------------------------------------------------------------------------------- /tests/integration/integration_config.yml.template: -------------------------------------------------------------------------------- 1 | # Replace the values to suit your CI environment 2 | host: ... 3 | test_subnet: ... 4 | test_domain: ... 5 | token_key: ... 6 | token_secret: ... 7 | customer_key: ... 8 | test_distro_series: ... 9 | test_existing_vm_host: ... 10 | test_lxd_host: 11 | hostname: ... 12 | power_type: ... 13 | power_driver: ... 14 | power_boot_type: ... 15 | power_address: ... 16 | power_user: ... 17 | power_pass: ... 18 | mac_address: ... 19 | architecture: ... 20 | test_virsh_host: 21 | hostname: ... 22 | power_type: ... 23 | power_driver: ... 24 | power_boot_type: ... 25 | power_address: ... 26 | power_user: ... 27 | power_pass: ... 28 | mac_address: ... 29 | architecture: ... 30 | -------------------------------------------------------------------------------- /examples/network_interface_link.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Connect nic to machine 3 | hosts: localhost 4 | tasks: 5 | - name: Create new link on sunny-raptor host with VM new-machine 6 | maas.maas.network_interface_link: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | machine: integration-test-nic-link.maas 13 | state: present 14 | mode: AUTO 15 | network_interface: test_nic 16 | subnet: 10.10.10.0/24 17 | ip_address: 10.10.10.4 18 | register: nic_info 19 | 20 | - ansible.builtin.debug: 21 | var: nic_info 22 | -------------------------------------------------------------------------------- /plugins/module_utils/cluster_instance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | 11 | from .client import Client 12 | 13 | 14 | def get_oauth1_client(params): 15 | cluster_instance = params["cluster_instance"] 16 | host = cluster_instance["host"] 17 | consumer_key = cluster_instance["customer_key"] 18 | token_key = cluster_instance["token_key"] 19 | token_secret = cluster_instance["token_secret"] 20 | 21 | client = Client(host, token_key, token_secret, consumer_key) 22 | return client 23 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | Modules 2 | ======= 3 | 4 | While different modules perform different tasks, their interfaces all follow 5 | the same pattern as much as possible. For instance, all Hypercore action modules 6 | do not support ``check_mode``, most of them can have their state set to either 7 | ``present`` or ``absent``, and they identify the resource to operate by using 8 | the *name* (or equivalent) parameter. 9 | 10 | The API of each module is composed of two parts. The *cluster_instance* parameter contains 11 | the pieces of information that are related to the Hypercore backend that the 12 | module is connecting to. All other parameters hold the information related to 13 | the resource that we are operating on. 14 | 15 | 16 | Authentication parameters 17 | ------------------------- 18 | 19 | Description about instance parameter. 20 | 21 | Module reference 22 | ---------------- 23 | 24 | .. toctree:: 25 | :glob: 26 | :maxdepth: 1 27 | 28 | modules/* 29 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Collections Plugins Directory 2 | 3 | This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that 4 | is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that 5 | would contain module utils and modules respectively. 6 | 7 | Here is an example directory of the majority of plugins currently supported by Ansible: 8 | 9 | ``` 10 | └── plugins 11 | ├── action 12 | ├── become 13 | ├── cache 14 | ├── callback 15 | ├── cliconf 16 | ├── connection 17 | ├── filter 18 | ├── httpapi 19 | ├── inventory 20 | ├── lookup 21 | ├── module_utils 22 | ├── modules 23 | ├── netconf 24 | ├── shell 25 | ├── strategy 26 | ├── terminal 27 | ├── test 28 | └── vars 29 | ``` 30 | 31 | A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.13/plugins/plugins.html). 32 | -------------------------------------------------------------------------------- /examples/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Use user module 3 | hosts: localhost 4 | tasks: 5 | - name: Create user James 6 | maas.maas.user: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | state: present 13 | name: James 14 | password: james123 15 | email: james@email.com 16 | is_admin: false 17 | register: users 18 | - ansible.builtin.debug: 19 | var: users 20 | 21 | - name: Delete user James 22 | maas.maas.user: 23 | cluster_instance: 24 | host: http://10.44.240.10:5240/MAAS 25 | token_key: kDcKvtWX7fXLB7TvB2 26 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 27 | customer_key: tqDErtYzyzRVdUb9hS 28 | state: absent 29 | name: James 30 | is_admin: false 31 | register: users 32 | - ansible.builtin.debug: 33 | var: users 34 | -------------------------------------------------------------------------------- /examples/vm_host_machine.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test query machines 3 | hosts: localhost 4 | tasks: 5 | - name: Create new machine on sunny-raptor host 6 | maas.maas.vm_host_machine: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | vm_host: crisp-skunk 13 | hostname: new-machine-3 14 | cores: 2 15 | # pinned_cores: 3 16 | memory: 2048 17 | zone: 1 18 | pool: 0 19 | domain: 0 20 | network_interfaces: # Compose allows for one network interface only. 21 | label_name: my-net 22 | name: lxdbr0 23 | subnet_cidr: "10.10.10.0/24" 24 | fabric: fabric-1 25 | vlan: 0 26 | ip_address: "10.10.10.150" 27 | storage_disks: 28 | - size_gigabytes: 3 29 | - size_gigabytes: 5 30 | register: machine 31 | 32 | - ansible.builtin.debug: 33 | var: machine 34 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory/tests/test_status_no.yml: -------------------------------------------------------------------------------- 1 | - name: Get variable info for every machine in the inventory - No status 2 | ansible.builtin.debug: 3 | msg: "hostvars.keys = {{ hostvars.keys() }}" 4 | 5 | - name: Check machineX in inventory - No status 6 | ansible.builtin.assert: 7 | that: 8 | - "{{ 'localhost' in hostvars }}" 9 | - "{{ ('inventory-test-1.' + test_domain) in hostvars }}" 10 | - "{{ ('inventory-test-2.' + test_domain) in hostvars }}" 11 | 12 | - name: Check Host in inventory from machineX - No status 13 | ansible.builtin.assert: 14 | that: 15 | - "{{ hostvars['inventory-test-1.' + test_domain]['ansible_host'] == 'inventory-test-1.' + test_domain }}" 16 | - "{{ hostvars['inventory-test-2.' + test_domain]['ansible_host'] == 'inventory-test-2.' + test_domain }}" 17 | 18 | - name: Check Group in inventory from machineX - No status 19 | ansible.builtin.assert: 20 | that: 21 | - "{{ hostvars['inventory-test-1.' + test_domain]['ansible_group'] == test_domain }}" 22 | - "{{ hostvars['inventory-test-2.' + test_domain]['ansible_group'] == test_domain }}" 23 | -------------------------------------------------------------------------------- /.github/workflows/ci-test.yml: -------------------------------------------------------------------------------- 1 | name: CI tests 2 | on: 3 | - push 4 | - pull_request 5 | env: 6 | REPO_DIR: ansible_collections/maas/maas 7 | jobs: 8 | docs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | path: ${{ env.REPO_DIR }} 15 | - name: Build docs 16 | run: | 17 | pip install tox 18 | env -C "$GITHUB_WORKSPACE/$REPO_DIR" -- tox -e docs 19 | 20 | sanity-test: 21 | runs-on: ubuntu-22.04 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | path: ${{ env.REPO_DIR }} 27 | - name: Sanity check 28 | run: | 29 | pip install tox 30 | env -C "$GITHUB_WORKSPACE/$REPO_DIR" -- tox -e sanity 31 | 32 | units-test: 33 | runs-on: ubuntu-22.04 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | with: 38 | path: ${{ env.REPO_DIR }} 39 | - name: Run unit tests 40 | run: | 41 | pip install tox 42 | env -C "$GITHUB_WORKSPACE/$REPO_DIR" -- tox -e coverage 43 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory/runme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | readonly vars_file=../../integration_config.yml 4 | 5 | eval "$(cat < x.name).includes(special)) { 27 | return 28 | } 29 | 30 | github.rest.issues.removeLabel({ 31 | issue_number: context.issue.number, 32 | owner: context.repo.owner, 33 | repo: context.repo.repo, 34 | name: [special] 35 | }) 36 | -------------------------------------------------------------------------------- /plugins/module_utils/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | 11 | from random import SystemRandom 12 | import time 13 | 14 | rand_instance = SystemRandom() 15 | 16 | 17 | def get_timestamp(): 18 | return str(int(time.time())) 19 | 20 | 21 | def get_nonce(timestamp: str): 22 | return str(rand_instance.getrandbits(64)) + timestamp 23 | 24 | 25 | def combine_item(key, value): 26 | return f'{key}="{value}"' 27 | 28 | 29 | def get_oauth_header(consumer_key, token_key, token_signature): 30 | timestamp = get_timestamp() 31 | nonce = get_nonce(timestamp) 32 | 33 | params = [ 34 | ("oauth_nonce", nonce), 35 | ("oauth_timestamp", timestamp), 36 | ("oauth_version", "1.0"), 37 | ("oauth_signature_method", "PLAINTEXT"), 38 | ("oauth_consumer_key", consumer_key), 39 | ("oauth_token", token_key), 40 | ("oauth_signature", f"%26{token_signature}"), 41 | ] 42 | 43 | partial = ", ".join(combine_item(k, v) for k, v in params) 44 | return f"OAuth {partial}" 45 | -------------------------------------------------------------------------------- /plugins/module_utils/form.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import random 11 | 12 | from .errors import MaasError 13 | 14 | 15 | class Multipart: 16 | SAFE_CHARS = "0123456789abcdefghijklmnoprstuvzABCDEFGHIJKLMNOPRSTUVZ" 17 | RN = "\r\n" 18 | 19 | @staticmethod 20 | def generate_boundary(): 21 | boundary = "" 22 | for i in range(0, 32): 23 | boundary += random.choice(Multipart.SAFE_CHARS) 24 | return boundary 25 | 26 | @staticmethod 27 | def get_mulipart(data): 28 | rn = Multipart.RN 29 | boundary = Multipart.generate_boundary() 30 | if not isinstance(data, dict): 31 | raise MaasError("Data should be dict!") 32 | 33 | content = "" 34 | for k, v in data.items(): 35 | content += f"--{boundary}{rn}" 36 | content += f'Content-Disposition: form-data; name="{k}"{rn}{rn}' 37 | content += str(v) 38 | content += rn 39 | content += f"--{boundary}--" 40 | return boundary, content.encode("utf-8") 41 | -------------------------------------------------------------------------------- /examples/network_interface_physical.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create nic and Delete nic 3 | hosts: localhost 4 | tasks: 5 | - name: Create new nic on crisp-skunk host with VM new-machine 6 | maas.maas.network_interface_physical: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | machine: calm-guinea.maas 13 | state: present 14 | mac_address: '00:16:3e:ae:78:76' 15 | name: test_nic 16 | mtu: 1700 17 | tags: 18 | - first 19 | - second 20 | vlan: 5002 21 | register: nic_info 22 | 23 | - ansible.builtin.debug: 24 | var: nic_info 25 | 26 | - name: Delete nic from machine calm-guinea on host sunny-raptor 27 | maas.maas.network_interface_physical: 28 | cluster_instance: 29 | host: http://10.44.240.10:5240/MAAS 30 | token_key: kDcKvtWX7fXLB7TvB2 31 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 32 | customer_key: tqDErtYzyzRVdUb9hS 33 | machine: calm-guinea.maas 34 | state: absent 35 | mac_address: '00:16:3e:ae:78:75' 36 | register: nic_info 37 | 38 | - ansible.builtin.debug: 39 | var: nic_info 40 | -------------------------------------------------------------------------------- /plugins/module_utils/state.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | import enum 12 | 13 | 14 | class HostState(str, enum.Enum): 15 | ready = "ready" 16 | absent = "absent" 17 | deploy = "deploy" 18 | 19 | 20 | class TaskState(str, enum.Enum): 21 | ready = "Ready" 22 | comissioning = "Commissioning" 23 | 24 | 25 | class NicState(str, enum.Enum): 26 | present = "present" 27 | absent = "absent" 28 | 29 | 30 | class TagState(str, enum.Enum): 31 | present = "present" 32 | absent = "absent" 33 | set = "set" 34 | 35 | 36 | class MachineTaskState(str, enum.Enum): 37 | allocated = "Allocated" 38 | new = "New" 39 | broken = "Broken" 40 | deployed = "Deployed" 41 | ready = "Ready" 42 | comissioning = "Commissioning" 43 | failed = "Failed" 44 | deploying = "Deploying" 45 | allocating = "Allocating" 46 | testing = "Testing" 47 | failed_comissioning = "Failed commissioning" 48 | failed_deployment = "Failed deployment" 49 | failed_testing = "Failed testing" 50 | 51 | 52 | class UserState(str, enum.Enum): 53 | present = "present" 54 | absent = "absent" 55 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_space.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils.space import Space 15 | 16 | pytestmark = pytest.mark.skipif( 17 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 18 | ) 19 | 20 | 21 | class TestMapper: 22 | def test_from_maas(self): 23 | maas_space_dict = dict( 24 | name="space-name", 25 | id="space-id", 26 | vlans=["vlans"], 27 | resource_uri="resource_uri", 28 | subnets=["subnets"], 29 | ) 30 | space = Space( 31 | maas_space_dict["name"], 32 | maas_space_dict["id"], 33 | maas_space_dict["vlans"], 34 | maas_space_dict["resource_uri"], 35 | maas_space_dict["subnets"], 36 | ) 37 | results = Space.from_maas(maas_space_dict) 38 | assert results.name == space.name 39 | assert results.id == space.id 40 | assert results.vlans == space.vlans 41 | assert results.resource_uri == space.resource_uri 42 | assert results.subnets == space.subnets 43 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_fabric.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils.fabric import Fabric 15 | 16 | pytestmark = pytest.mark.skipif( 17 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 18 | ) 19 | 20 | 21 | class TestMapper: 22 | def test_from_maas(self): 23 | maas_fabric_dict = dict( 24 | name="fabric-name", 25 | id="fabric-id", 26 | vlans=["vlans"], 27 | resource_uri="resource_uri", 28 | class_type="class_type", 29 | ) 30 | fabric = Fabric( 31 | maas_fabric_dict["name"], 32 | maas_fabric_dict["id"], 33 | maas_fabric_dict["vlans"], 34 | maas_fabric_dict["resource_uri"], 35 | maas_fabric_dict["class_type"], 36 | ) 37 | results = Fabric.from_maas(maas_fabric_dict) 38 | assert results.name == fabric.name 39 | assert results.id == fabric.id 40 | assert results.vlans == fabric.vlans 41 | assert results.resource_uri == fabric.resource_uri 42 | assert results.class_type == fabric.class_type 43 | -------------------------------------------------------------------------------- /plugins/module_utils/arguments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | 11 | from ansible.module_utils.basic import env_fallback 12 | 13 | SHARED_SPECS = dict( 14 | cluster_instance=dict( 15 | type="dict", 16 | apply_defaults=True, 17 | options=dict( 18 | host=dict( 19 | type="str", 20 | required=True, 21 | fallback=(env_fallback, ["MAAS_HOST"]), 22 | ), 23 | token_key=dict( 24 | type="str", 25 | required=True, 26 | no_log=True, 27 | fallback=(env_fallback, ["MAAS_TOKEN_KEY"]), 28 | ), 29 | token_secret=dict( 30 | type="str", 31 | required=True, 32 | no_log=True, 33 | fallback=(env_fallback, ["MAAS_TOKEN_SECRET"]), 34 | ), 35 | customer_key=dict( 36 | type="str", 37 | required=True, 38 | no_log=True, 39 | fallback=(env_fallback, ["MAAS_CUSTOMER_KEY"]), 40 | ), 41 | ), 42 | ) 43 | ) 44 | 45 | 46 | def get_spec(*param_names): 47 | return dict((p, SHARED_SPECS[p]) for p in param_names) 48 | -------------------------------------------------------------------------------- /plugins/module_utils/dns_record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | 12 | def to_ansible(record, is_resource_record=False): 13 | resource_records = ( 14 | [record] if is_resource_record else record.get("resource_records", []) 15 | ) 16 | name, domain = record["fqdn"].rsplit(".", 1) 17 | 18 | if resource_records: 19 | result = [ 20 | { 21 | "type": item["rrtype"], 22 | "data": item["rrdata"], 23 | "fqdn": record["fqdn"], 24 | "name": name, 25 | "domain": domain, 26 | "ttl": item["ttl"], 27 | "id": item["id"], 28 | } 29 | for item in resource_records 30 | ] 31 | return result 32 | 33 | ip_addresses = [x["ip"] for x in record["ip_addresses"] if x["ip"]] 34 | if ip_addresses: 35 | return [ 36 | { 37 | "type": "A/AAAA", 38 | "data": " ".join(ip_addresses), 39 | "fqdn": record["fqdn"], 40 | "name": name, 41 | "domain": domain, 42 | "ttl": record["address_ttl"], 43 | "id": record["id"], 44 | } 45 | ] 46 | return [] 47 | -------------------------------------------------------------------------------- /plugins/module_utils/task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | from time import sleep 12 | 13 | from ..module_utils.rest_client import RestClient 14 | from ..module_utils.state import TaskState 15 | 16 | 17 | class Task: 18 | @classmethod 19 | def wait_task(cls, client, device, check_mode=False): 20 | if check_mode: 21 | return 22 | while True: 23 | task_status = Task.get_task_status(client, device, id) 24 | if not task_status: # No such task_status is found 25 | break 26 | if ( 27 | task_status.get("status_name", "") == TaskState.ready 28 | ): # Task has finished 29 | break 30 | # TODO: Add other states like Error or not complete etc... 31 | sleep(1) 32 | 33 | @staticmethod 34 | def get_task_status(client, device, id): 35 | rest_client = RestClient(client=client) 36 | endpoint = "" 37 | if device == "host": 38 | endpoint = f"/api/2.0/machines/{id}/" 39 | elif device == "machine": 40 | endpoint = f"/api/2.0/vm-hosts/{id}/" 41 | # TODO: Add other endpoints 42 | task_status = rest_client.get_record(endpoint) 43 | return task_status if task_status else {} 44 | -------------------------------------------------------------------------------- /plugins/module_utils/tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | from ..module_utils import errors 11 | 12 | 13 | class Tag: 14 | @staticmethod 15 | def send_tag_request(client, machine_id, tag_name): 16 | payload = dict(add=machine_id) 17 | client.post( 18 | f"/api/2.0/tags/{tag_name}/", 19 | query={"op": "update_nodes"}, 20 | data=payload, 21 | ).json 22 | 23 | @staticmethod 24 | def send_untag_request(client, machine_id, tag_name): 25 | payload = dict(remove=machine_id) 26 | client.post( 27 | f"/api/2.0/tags/{tag_name}/", 28 | query={"op": "update_nodes"}, 29 | data=payload, 30 | ).json 31 | 32 | @staticmethod 33 | def get_tag_by_name(client, module, must_exist=False): 34 | response = client.get("/api/2.0/tags/").json 35 | for tag in response: 36 | if tag["name"] == module.params["name"]: 37 | return tag 38 | if must_exist: 39 | raise errors.MaasError( 40 | f"Tag - {module.params['name']} - does not exist." 41 | ) 42 | 43 | @staticmethod 44 | def send_create_request(client, module): 45 | payload = dict(name=module.params["name"]) 46 | client.post( 47 | "/api/2.0/tags/", 48 | data=payload, 49 | ) 50 | -------------------------------------------------------------------------------- /plugins/doc_fragments/cluster_instance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ 5 | # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | 12 | class ModuleDocFragment(object): 13 | DOCUMENTATION = r""" 14 | options: 15 | cluster_instance: 16 | description: 17 | - Canonical MAAS instance information. 18 | type: dict 19 | suboptions: 20 | host: 21 | description: 22 | - The MAAS instance url. 23 | - If not set, the value of the C(MAAS_HOST) environment 24 | variable will be used. 25 | - For example "http://localhost:5240/MAAS". 26 | required: true 27 | type: str 28 | token_key: 29 | description: 30 | - Token key used for authentication. 31 | - If not set, the value of the C(MAAS_TOKEN_KEY) environment 32 | variable will be used. 33 | required: true 34 | type: str 35 | token_secret: 36 | description: 37 | - Token secret used for authentication. 38 | - If not set, the value of the C(MAAS_TOKEN_SECRET) environment 39 | variable will be used. 40 | required: true 41 | type: str 42 | customer_key: 43 | description: 44 | - Client secret used for authentication. 45 | - If not set, the value of the C(MAAS_CUSTOMER_KEY) environment 46 | variable will be used. 47 | required: true 48 | type: str 49 | """ 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Make sure we have ansible_collections/servicenow/itsm as a prefix. This is 2 | # ugly as hack, but it works. I suggest all future developer to treat next few 3 | # lines as an opportunity to learn a thing or two about GNU make ;) 4 | collection := $(notdir $(realpath $(CURDIR) )) 5 | namespace := $(notdir $(realpath $(CURDIR)/.. )) 6 | toplevel := $(notdir $(realpath $(CURDIR)/../..)) 7 | 8 | err_msg := Place collection at /ansible_collections/maas/maas 9 | ifeq (true,$(CI)) 10 | $(info Running in CI setting, skipping directory checks.) 11 | else ifneq (maas, $(collection)) 12 | $(error $(err_msg)) 13 | else ifneq (maas, $(namespace)) 14 | $(error $(err_msg)) 15 | else ifneq (ansible_collections, $(toplevel)) 16 | $(error $(err_msg)) 17 | endif 18 | 19 | python_version := $(shell \ 20 | python -c 'import sys; print(".".join(map(str, sys.version_info[:2])))' \ 21 | ) 22 | 23 | unit_test_targets := $(shell find tests/unit -name '*.py') 24 | integration_test_targets := $(shell ls tests/integration/targets) 25 | 26 | 27 | .PHONY: help 28 | help: 29 | @echo Available targets: 30 | @fgrep "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sort 31 | 32 | # Developer convenience targets 33 | 34 | .PHONY: clean 35 | clean: ## Remove all auto-generated files 36 | rm -rf tests/output 37 | 38 | .PHONY: $(integration_test_targets) 39 | $(integration_test_targets): 40 | ansible-test integration --requirements --python $(python_version) --diff $@ 41 | 42 | .PHONY: integration 43 | integration: ## Run integration tests 44 | ansible-test integration --docker --diff 45 | 46 | .PHONY: integration-local 47 | integration-local: 48 | ansible-test integration --local --diff 49 | -------------------------------------------------------------------------------- /plugins/module_utils/disk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | from . import errors 12 | from .utils import MaasValueMapper 13 | 14 | __metaclass__ = type 15 | 16 | 17 | class Disk(MaasValueMapper): 18 | def __init__( 19 | # Add more values as needed. 20 | self, 21 | name=None, 22 | id=None, 23 | size=None, 24 | ): 25 | self.name = name 26 | self.id = id 27 | self.size = size 28 | 29 | @classmethod 30 | def from_ansible(cls, module): 31 | obj = Disk() 32 | obj.size = module["size_gigabytes"] 33 | return obj 34 | 35 | @classmethod 36 | def from_maas(cls, maas_dict): 37 | obj = Disk() 38 | try: 39 | obj.name = maas_dict["name"] 40 | obj.id = maas_dict["id"] 41 | obj.size = int(int(maas_dict["size"]) / 1000000000) 42 | except KeyError as e: 43 | raise errors.MissingValueMAAS(e) 44 | return obj 45 | 46 | def to_maas(self): 47 | to_maas_dict = {} 48 | if self.id: 49 | to_maas_dict["id"] = self.id 50 | if self.name: 51 | to_maas_dict["name"] = self.name 52 | if self.size: 53 | to_maas_dict["size"] = self.size 54 | return to_maas_dict 55 | 56 | def to_ansible(self): 57 | return dict( 58 | id=self.id, 59 | name=self.name, 60 | size_gigabytes=self.size, 61 | ) 62 | -------------------------------------------------------------------------------- /plugins/modules/boot_sources_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: boot_sources_info 13 | 14 | author: 15 | - Jure Medvesek (@juremedvesek) 16 | short_description: Return info about boot sources. 17 | description: 18 | - Plugin returns information about available boot sources. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: {} 24 | """ 25 | 26 | EXAMPLES = r""" 27 | - name: List boot sources 28 | maas.maas.vm_host_info: 29 | cluster_instance: 30 | host: ... 31 | token_key: ... 32 | token_secret: ... 33 | customer_key: ... 34 | """ 35 | 36 | RETURN = r""" 37 | records: 38 | description: 39 | - Boot sources info list. 40 | returned: success 41 | type: list 42 | sample: # ADD SAMPLE 43 | """ 44 | 45 | 46 | from ansible.module_utils.basic import AnsibleModule 47 | 48 | from ..module_utils import arguments, errors 49 | from ..module_utils.client import Client 50 | from ..module_utils.cluster_instance import get_oauth1_client 51 | 52 | 53 | def run(module, client: Client): 54 | response = client.get("/api/2.0/boot-resources/") 55 | return response.json 56 | 57 | 58 | def main(): 59 | module = AnsibleModule( 60 | supports_check_mode=True, 61 | argument_spec=dict( 62 | arguments.get_spec("cluster_instance"), 63 | ), 64 | ) 65 | 66 | try: 67 | client = get_oauth1_client(module.params) 68 | records = run(module, client) 69 | module.exit_json(changed=False, records=records) 70 | except errors.MaasError as e: 71 | module.fail_json(msg=str(e)) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /plugins/modules/dns_domain_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: dns_domain_info 13 | 14 | author: 15 | - Jure Medvesek (@juremedvesek) 16 | short_description: List DNS domains. 17 | description: 18 | - Plugin returns information about available DNS domains. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: {} 24 | """ 25 | 26 | EXAMPLES = r""" 27 | - name: List domains 28 | maas.maas.dns_domain_info: 29 | cluster_instance: 30 | host: ... 31 | token_key: ... 32 | token_secret: ... 33 | customer_key: ... 34 | """ 35 | 36 | RETURN = r""" 37 | record: 38 | description: 39 | - List of all domains. 40 | returned: success 41 | type: list 42 | sample: 43 | - authoritative: true 44 | id: 0 45 | is_default: true 46 | name: maas 47 | ttl: null 48 | """ 49 | 50 | 51 | from ansible.module_utils.basic import AnsibleModule 52 | 53 | from ..module_utils import arguments, errors 54 | from ..module_utils.client import Client 55 | from ..module_utils.cluster_instance import get_oauth1_client 56 | 57 | 58 | def run(client: Client): 59 | response = client.get("/api/2.0/domains/") 60 | return response.json 61 | 62 | 63 | def main(): 64 | module = AnsibleModule( 65 | supports_check_mode=True, 66 | argument_spec=dict( 67 | arguments.get_spec("cluster_instance"), 68 | ), 69 | ) 70 | 71 | try: 72 | client = get_oauth1_client(module.params) 73 | records = run(client) 74 | module.exit_json(changed=False, records=records) 75 | except errors.MaasError as ex: 76 | module.fail_json(msg=str(ex)) 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Collection - maas.maas 2 | 3 | ## Getting Started 4 | 5 | To start using this collection, install `ansible` or `ansible-core` packages. On a Debian or Ubuntu machine this can be done using: 6 | ``` 7 | sudo apt-get update 8 | sudo apt-get -y install ansible-core 9 | ``` 10 | 11 | Then issue the following commmand to install the collection: 12 | ``` 13 | ansible-galaxy collection install maas.maas 14 | ``` 15 | 16 | Alternatively, you can install the collection directly from Github: 17 | ``` 18 | ansible-galaxy collection install git+https://github.com/canonical/ansible-collection.git 19 | ``` 20 | 21 | To verify the installation, issue the following command: 22 | ``` 23 | ansible-galaxy collection list | grep maas 24 | ``` 25 | 26 | ## Sample Playbook 27 | 28 | The following example demonstrates a very simple playbook to read a machine information using `fqdn` from MAAS: 29 | 30 | ```yaml 31 | --- 32 | - name: Read a machine info 33 | hosts: localhost 34 | tasks: 35 | - name: List machines 36 | maas.maas.machine_info: 37 | cluster_instance: 38 | host: http://maas.example.com:5240/MAAS 39 | token_key: RK3XxE598ubXqvPnyq 40 | token_secret: JspVhytBzxVtSwzhmMczJTvT5kAVkMFx 41 | customer_key: E3CAjFSXtQAvqufCTZ 42 | fqdn: example-node-1.maas 43 | register: machines 44 | - ansible.builtin.debug: 45 | var: machines 46 | ``` 47 | 48 | Required information for the above template: 49 | 50 | * *host* is the MAAS endpoint. 51 | * *token_key*, *token_secret* and *customer_key* can be obtained from MAAS CLI: 52 | ``` 53 | sudo maas apikey --username admin 54 | ``` 55 | Example output: 56 | ``` 57 | E3CAjFSXtQAvqufCTZ:RK3XxE598ubXqvPnyq:JspVhytBzxVtSwzhmMczJTvT5kAVkMFx 58 | ``` 59 | * *fqdn* is the FQDN for a target machine. 60 | 61 | To execute the playbook, issue the following command: 62 | ``` 63 | ansible-playbook sample.yaml 64 | ``` 65 | 66 | ## Published Documentation 67 | 68 | Read the current documentation directly on [Ansible Galaxy](https://galaxy.ansible.com/ui/repo/published/maas/maas/). 69 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_disk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils.disk import Disk 15 | 16 | pytestmark = pytest.mark.skipif( 17 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 18 | ) 19 | 20 | 21 | class TestMapper: 22 | @staticmethod 23 | def _get_disk(): 24 | return dict(name="test_disk", id=123, size=500000000000) 25 | 26 | @staticmethod 27 | def _get_disk_from_ansible(): 28 | return dict(size_gigabytes=5) 29 | 30 | def test_from_maas(self): 31 | maas_disk_dict = self._get_disk() 32 | disk_obj = Disk( 33 | maas_disk_dict["name"], 34 | maas_disk_dict["id"], 35 | maas_disk_dict["size"] / 1000000000, 36 | ) 37 | results = Disk.from_maas(maas_disk_dict) 38 | assert ( 39 | results.name == disk_obj.name 40 | and results.id == disk_obj.id 41 | and results.size == disk_obj.size 42 | ) 43 | 44 | def test_from_ansible(self): 45 | disk_dict = self._get_disk_from_ansible() 46 | disk_obj = Disk(size=disk_dict["size_gigabytes"]) 47 | results = Disk.from_ansible(disk_dict) 48 | assert results.size == disk_obj.size 49 | 50 | def test_to_maas(self): 51 | disk_dict = self._get_disk() 52 | expected = dict(name="test_disk", id=123, size=500000000000) 53 | disk_obj = Disk(disk_dict["name"], disk_dict["id"], disk_dict["size"]) 54 | results = disk_obj.to_maas() 55 | assert results == expected 56 | 57 | def test_to_ansible(self): 58 | disk_dict = self._get_disk() 59 | expected = dict(id=123, name="test_disk", size_gigabytes=500) 60 | disk_obj = Disk(disk_dict["name"], disk_dict["id"], 500) 61 | results = disk_obj.to_ansible() 62 | assert results == expected 63 | -------------------------------------------------------------------------------- /docs/templates/module.rst.j2: -------------------------------------------------------------------------------- 1 | .. _maas.maas.{{ module }}_module: 2 | 3 | {% set title = module + ' -- ' + short_description | rst_ify %} 4 | {{ title }} 5 | {{ '=' * title | length }} 6 | 7 | {% for desc in description %} 8 | {{ desc | rst_ify }} 9 | 10 | {% endfor %} 11 | 12 | {% if version_added is defined -%} 13 | .. versionadded:: {{ version_added }} 14 | {% endif %} 15 | 16 | {% if requirements -%} 17 | Requirements 18 | ------------ 19 | 20 | The below requirements are needed on the host that executes this module: 21 | 22 | {% for req in requirements %} 23 | - {{ req | rst_ify }} 24 | {% endfor %} 25 | {% endif %} 26 | 27 | 28 | Examples 29 | -------- 30 | 31 | .. code-block:: yaml+jinja 32 | 33 | {{ examples | indent(3, True) }} 34 | 35 | 36 | {% if notes -%} 37 | Notes 38 | ----- 39 | 40 | .. note:: 41 | {% for note in notes %} 42 | {{ note | rst_ify }} 43 | 44 | {% endfor %} 45 | {% endif %} 46 | 47 | 48 | {% if seealso -%} 49 | See Also 50 | -------- 51 | 52 | .. seealso:: 53 | 54 | {% for item in seealso %} 55 | - :ref:`{{ item.module }}_module` 56 | {% endfor %} 57 | {% endif %} 58 | 59 | 60 | {% macro option_desc(opts, level) %} 61 | {% for name, spec in opts | dictsort recursive %} 62 | {% set req = "required" if spec.required else "optional" %} 63 | {% set default = ", default: " ~ spec.default if spec.default else "" %} 64 | {{ " " * level }}{{ name }} ({{ req }}) 65 | {% for para in spec.description %} 66 | {{ " " * level }}{{ para | rst_ify }} 67 | 68 | {% endfor %} 69 | {{ " " * level }}| **type**: {{ spec.type | default("str") }} 70 | {% if spec.default %} 71 | {{ " " * level }}| **default**: {{ spec.default }} 72 | {% endif %} 73 | {% if spec.choices %} 74 | {% set str_choices = spec.choices | map("string") %} 75 | {{ " " * level }}| **choices**: {{ ", ".join(str_choices) }} 76 | {% endif %} 77 | 78 | {% if spec.suboptions %} 79 | {{ option_desc(spec.suboptions, level + 1) }} 80 | {% endif %} 81 | {% endfor %} 82 | {% endmacro %} 83 | 84 | {% if options -%} 85 | Parameters 86 | ---------- 87 | 88 | {{ option_desc(options, 0) }} 89 | {% endif %} 90 | 91 | {% if returndocs -%} 92 | Return Values 93 | ------------- 94 | 95 | {% for name, spec in returndocs.items() %} 96 | {{ name }} 97 | {% for para in spec.description %} 98 | {{ para | rst_ify }} 99 | 100 | {% endfor %} 101 | **sample**: 102 | 103 | .. code-block:: yaml 104 | 105 | {{ spec.sample | to_yaml(default_flow_style=False, indent=2) | indent(6) }} 106 | {% endfor %} 107 | {% endif %} 108 | -------------------------------------------------------------------------------- /plugins/modules/tag_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: tag_info 13 | 14 | author: 15 | - Domen Dobnikar (@domen_dobnikar) 16 | short_description: Get list of all tags. 17 | description: Shows information about all tags on this MAAS. 18 | version_added: 1.0.0 19 | extends_documentation_fragment: 20 | - maas.maas.cluster_instance 21 | seealso: [] 22 | options: {} 23 | """ 24 | 25 | EXAMPLES = r""" 26 | - name: List tags 27 | maas.maas.tag_info: 28 | cluster_instance: 29 | host: http://10.44.240.10:5240/MAAS 30 | token_key: kDcKvtWX7fXLB7TvB2 31 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 32 | customer_key: tqDErtYzyzRVdUb9hS 33 | register: tags 34 | - ansible.builtin.debug: 35 | var: tags 36 | """ 37 | 38 | RETURN = r""" 39 | records: 40 | description: 41 | - Tag information. 42 | returned: success 43 | type: list 44 | sample: 45 | - comment: '' 46 | definition: '' 47 | kernel_opts: '' 48 | name: virtual 49 | resource_uri: /MAAS/api/2.0/tags/virtual/ 50 | - comment: '' 51 | definition: '' 52 | kernel_opts: console=tty1 console=ttyS0 53 | name: pod-console-logging 54 | resource_uri: /MAAS/api/2.0/tags/pod-console-logging/ 55 | - comment: my-tag 56 | definition: '' 57 | kernel_opts: '' 58 | name: my-tag 59 | resource_uri: /MAAS/api/2.0/tags/my-tag/ 60 | - comment: my-tag2 61 | definition: '' 62 | kernel_opts: '' 63 | name: my-tag2 64 | resource_uri: /MAAS/api/2.0/tags/my-tag2/ 65 | """ 66 | 67 | from ansible.module_utils.basic import AnsibleModule 68 | 69 | from ..module_utils import arguments, errors 70 | from ..module_utils.cluster_instance import get_oauth1_client 71 | 72 | 73 | def run(module, client): 74 | response = client.get("/api/2.0/tags/") 75 | return response.json 76 | 77 | 78 | def main(): 79 | module = AnsibleModule( 80 | supports_check_mode=True, 81 | argument_spec=dict( 82 | arguments.get_spec("cluster_instance"), 83 | ), 84 | ) 85 | 86 | try: 87 | client = get_oauth1_client(module.params) 88 | records = run(module, client) 89 | module.exit_json(changed=False, records=records) 90 | except errors.MaasError as e: 91 | module.fail_json(msg=str(e)) 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ### REQUIRED 3 | # The namespace of the collection. This can be a company/brand/organization or product namespace under which all 4 | # content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with 5 | # underscores or numbers and cannot contain consecutive underscores 6 | namespace: maas 7 | 8 | # The name of the collection. Has the same character restrictions as 'namespace' 9 | name: maas 10 | 11 | # The version of the collection. Must be compatible with semantic versioning 12 | version: 1.0.0 13 | 14 | # The path to the Markdown (.md) readme file. This path is relative to the root of the collection 15 | readme: README.md 16 | 17 | # A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) 18 | # @nicks:irc/im.site#channel' 19 | authors: 20 | - Jure Medvesek 21 | - Christian Grabowski 22 | 23 | 24 | ### OPTIONAL but strongly recommended 25 | # A short summary description of the collection 26 | description: An Ansible Collection for configuring and managing MAAS 27 | 28 | # The path to the license file for the collection. This path is relative to the root of the collection. This key is 29 | # mutually exclusive with 'license' 30 | license_file: 'LICENSE' 31 | 32 | # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character 33 | # requirements as 'namespace' and 'name' 34 | tags: 35 | - infrastructure 36 | 37 | # Collections that this collection requires to be installed for it to be usable. The key of the dict is the 38 | # collection label 'namespace.name'. The value is a version range 39 | # L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version 40 | # range specifiers can be set and are separated by ',' 41 | dependencies: {} 42 | 43 | # The URL of the originating SCM repository 44 | repository: https://github.com/canonical/ansible-collection 45 | 46 | # The URL to any online docs 47 | documentation: https://github.com/canonical/ansible-collection 48 | 49 | # The URL to the homepage of the collection/project 50 | homepage: https://maas.io 51 | 52 | # The URL to the collection issue tracker 53 | issues: https://github.com/canonical/ansible-collection/issues 54 | 55 | # A list of file glob-like patterns used to filter any files or directories that should not be included in the build 56 | # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This 57 | # uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', 58 | # and '.git' are always filtered 59 | build_ignore: [] 60 | -------------------------------------------------------------------------------- /examples/tag.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create new tag and place it on VMs 3 | hosts: localhost 4 | tasks: 5 | - name: Create new tag 'bla' and place it on VMs 6 | maas.maas.tag: 7 | cluster_instance: 8 | host: http://10.44.240.10:5240/MAAS 9 | token_key: kDcKvtWX7fXLB7TvB2 10 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 11 | customer_key: tqDErtYzyzRVdUb9hS 12 | state: present 13 | name: bla 14 | machines: 15 | - instance-test.maas 16 | - new-machine-test-tag.maas 17 | register: tag_list 18 | - ansible.builtin.debug: 19 | var: tag_list 20 | 21 | - name: Delete new tag 'bla' 22 | maas.maas.tag: 23 | cluster_instance: 24 | host: http://10.44.240.10:5240/MAAS 25 | token_key: kDcKvtWX7fXLB7TvB2 26 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 27 | customer_key: tqDErtYzyzRVdUb9hS 28 | state: absent 29 | name: bla 30 | machines: 31 | - instance-test.maas 32 | - new-machine-test-tag.maas 33 | register: tag_list 34 | - ansible.builtin.debug: 35 | var: tag_list 36 | 37 | - name: Set new tag 'bla' and place it on one VM 38 | maas.maas.tag: 39 | cluster_instance: 40 | host: http://10.44.240.10:5240/MAAS 41 | token_key: kDcKvtWX7fXLB7TvB2 42 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 43 | customer_key: tqDErtYzyzRVdUb9hS 44 | state: set 45 | name: bla 46 | machines: 47 | - instance-test.maas 48 | register: tag_list 49 | - ansible.builtin.debug: 50 | var: tag_list 51 | 52 | - name: Set new tag 'bla' and place it on the other VM and remove it from current 53 | maas.maas.tag: 54 | cluster_instance: 55 | host: http://10.44.240.10:5240/MAAS 56 | token_key: kDcKvtWX7fXLB7TvB2 57 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 58 | customer_key: tqDErtYzyzRVdUb9hS 59 | state: set 60 | name: bla 61 | machines: 62 | - new-machine-test-tag.maas 63 | register: tag_list 64 | - ansible.builtin.debug: 65 | var: tag_list 66 | 67 | - name: Delete new tag 'bla' 68 | maas.maas.tag: 69 | cluster_instance: 70 | host: http://10.44.240.10:5240/MAAS 71 | token_key: kDcKvtWX7fXLB7TvB2 72 | token_secret: ktBqeLMRvLBDLFm7g8xybgpQ4jSkkwgk 73 | customer_key: tqDErtYzyzRVdUb9hS 74 | state: absent 75 | name: bla 76 | machines: 77 | - instance-test.maas 78 | - new-machine-test-tag.maas 79 | register: tag_list 80 | - ansible.builtin.debug: 81 | var: tag_list 82 | -------------------------------------------------------------------------------- /plugins/modules/dns_record_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: dns_record_info 13 | 14 | author: 15 | - Jure Medvesek (@juremedvesek) 16 | short_description: List DNS records. 17 | description: 18 | - Plugin returns information about available DNS records. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: 24 | all: 25 | description: Include implicit DNS records created for nodes registered in MAAS if true. 26 | type: bool 27 | required: false 28 | """ 29 | 30 | EXAMPLES = r""" 31 | - name: List records 32 | maas.maas.dns_records_info: 33 | cluster_instance: 34 | host: ... 35 | token_key: ... 36 | token_secret: ... 37 | customer_key: ... 38 | """ 39 | 40 | RETURN = r""" 41 | record: 42 | description: 43 | - List of all dns records. 44 | returned: success 45 | type: list 46 | sample: 47 | - records: 48 | - data: maas.io 49 | fqdn: cname-record.maas 50 | ttl: null 51 | type: CNAME 52 | - data: 192.168.0.1 53 | fqdn: test.maas 54 | ttl: 15 55 | type: A/AAAA 56 | - data: 10.0.0.1 10.0.0.2 57 | fqdn: test2.maas 58 | ttl: 5 59 | type: A/AAAA 60 | """ 61 | 62 | from ansible.module_utils.basic import AnsibleModule 63 | 64 | from ..module_utils import arguments, errors 65 | from ..module_utils.client import Client 66 | from ..module_utils.cluster_instance import get_oauth1_client 67 | from ..module_utils.dns_record import to_ansible 68 | 69 | 70 | def run(module, client: Client): 71 | query = {"all": True} if module.params["all"] else None 72 | response = client.get("/api/2.0/dnsresources/", query).json 73 | parsed = [] 74 | for items in response: 75 | partial = to_ansible(items) 76 | for item in partial: 77 | parsed.append(item) 78 | 79 | result = [x for x in parsed if x] 80 | return result 81 | 82 | 83 | def main(): 84 | module = AnsibleModule( 85 | supports_check_mode=True, 86 | argument_spec=dict( 87 | arguments.get_spec("cluster_instance"), 88 | all=dict(type="bool", required=False), 89 | ), 90 | ) 91 | 92 | try: 93 | client = get_oauth1_client(module.params) 94 | records = run(module, client) 95 | module.exit_json(changed=False, records=records) 96 | except errors.MaasError as ex: 97 | module.fail_json(msg=str(ex)) 98 | 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_vmhost.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils.client import Response 15 | from ansible_collections.maas.maas.plugins.module_utils.vmhost import VMHost 16 | 17 | pytestmark = pytest.mark.skipif( 18 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 19 | ) 20 | 21 | 22 | class TestSendComposeRequest: 23 | @staticmethod 24 | def _get_empty_host_dict(): 25 | return dict( 26 | name="test_name", 27 | id="1234", 28 | cpu_over_commit_ratio=3, 29 | memory_over_commit_ratio=3, 30 | default_macvlan_mode="default", 31 | tags=None, 32 | zone=1, 33 | pool=1, 34 | ) 35 | 36 | def test_send_compose_request(self, client, mocker): 37 | module = "" 38 | payload = "" 39 | vmhost_dict = self._get_empty_host_dict() 40 | vmhost_obj = VMHost.from_maas(vmhost_dict) 41 | client.post.return_value = Response( 42 | 200, '{"system_id":"123", "resource_uri":""}' 43 | ) 44 | results = vmhost_obj.send_compose_request(module, client, payload) 45 | assert results == {"system_id": "123", "resource_uri": ""} 46 | 47 | 48 | class TestMapper: 49 | @staticmethod 50 | def _get_host(): 51 | return dict( 52 | name="test_host", 53 | id=123, 54 | cpu_over_commit_ratio=3, 55 | memory_over_commit_ratio=2, 56 | default_macvlan_mode="default", 57 | tags=[], 58 | zone=1, 59 | pool=1, 60 | ) 61 | 62 | def test_from_maas(self): 63 | maas_host_dict = self._get_host() 64 | host = VMHost( 65 | maas_host_dict["name"], 66 | maas_host_dict["id"], 67 | maas_host_dict["cpu_over_commit_ratio"], 68 | maas_host_dict["memory_over_commit_ratio"], 69 | maas_host_dict["default_macvlan_mode"], 70 | maas_host_dict["pool"], 71 | maas_host_dict["zone"], 72 | maas_host_dict["tags"], 73 | ) 74 | results = VMHost.from_maas(maas_host_dict) 75 | assert results.name == host.name 76 | assert results.cpu_over_commit_ratio == host.cpu_over_commit_ratio 77 | assert ( 78 | results.memory_over_commit_ratio == host.memory_over_commit_ratio 79 | ) 80 | assert results.default_macvlan_mode == host.default_macvlan_mode 81 | assert results.id == host.id 82 | assert results.pool == host.pool 83 | assert results.zone == host.zone 84 | assert results.tags == host.tags 85 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory/common/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Create VMs needed for testing inventory plugin 3 | - name: Prepare test environment 4 | hosts: localhost 5 | gather_facts: false 6 | environment: 7 | MAAS_HOST: "{{ host }}" 8 | MAAS_TOKEN_KEY: "{{ token_key }}" 9 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 10 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 11 | vars: 12 | vm_host: "{% if test_existing_vm_host is defined %}{{ test_existing_vm_host }}{% else %}{{ test_lxd_host.hostname }}{% endif %}" 13 | 14 | tasks: 15 | - name: Create VM inventory-test-1. 16 | maas.maas.vm_host_machine: 17 | hostname: inventory-test-1 18 | vm_host: "{{ vm_host }}" 19 | cores: 2 20 | memory: 2048 21 | network_interfaces: 22 | label_name: my_first 23 | subnet_cidr: "{{ test_subnet }}" 24 | register: machine 25 | - ansible.builtin.assert: 26 | that: 27 | - machine is succeeded 28 | - machine is changed 29 | 30 | - name: Get info from inventory-test-1. 31 | maas.maas.machine_info: 32 | fqdn: "{{ machine.record.fqdn }}" 33 | register: machine_info 34 | - ansible.builtin.assert: 35 | that: 36 | - machine_info is succeeded 37 | - machine_info is not changed 38 | - machine_info.records 39 | - machine_info.records | length == 1 40 | - machine_info.records.0.fqdn == "inventory-test-1.{{ test_domain }}" 41 | - machine_info.records.0.cpu_count == 2 42 | - machine_info.records.0.interface_set | length == 1 43 | - machine_info.records.0.interface_set.0.name == "my_first" 44 | - machine_info.records.0.interface_set.0.links.0.subnet.cidr == "{{ test_subnet }}" 45 | - machine_info.records.0.memory == 2048 46 | 47 | - name: Create VM inventory-test-2. 48 | maas.maas.vm_host_machine: 49 | hostname: inventory-test-2 50 | vm_host: "{{ vm_host }}" 51 | cores: 2 52 | memory: 2048 53 | network_interfaces: 54 | label_name: my_first 55 | subnet_cidr: "{{ test_subnet }}" 56 | register: machine 57 | - ansible.builtin.assert: 58 | that: 59 | - machine is succeeded 60 | - machine is changed 61 | 62 | - name: Get info from inventory-test-2. 63 | maas.maas.machine_info: 64 | fqdn: "{{ machine.record.fqdn }}" 65 | register: machine_info 66 | - ansible.builtin.assert: 67 | that: 68 | - machine_info is succeeded 69 | - machine_info is not changed 70 | - machine_info.records 71 | - machine_info.records | length == 1 72 | - machine_info.records.0.fqdn == "inventory-test-2.{{ test_domain }}" 73 | - machine_info.records.0.cpu_count == 2 74 | - machine_info.records.0.interface_set | length == 1 75 | - machine_info.records.0.interface_set.0.name == "my_first" 76 | - machine_info.records.0.interface_set.0.links.0.subnet.cidr == "{{ test_subnet }}" 77 | - machine_info.records.0.memory == 2048 78 | -------------------------------------------------------------------------------- /plugins/module_utils/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | 11 | class MaasError(Exception): 12 | pass 13 | 14 | 15 | class AuthError(MaasError): 16 | pass 17 | 18 | 19 | class UnexpectedAPIResponse(MaasError): 20 | def __init__(self, response): 21 | self.message = "Unexpected response - {0} {1}".format( 22 | response.status, response.data 23 | ) 24 | super(UnexpectedAPIResponse, self).__init__(self.message) 25 | 26 | 27 | class InvalidUuidFormatError(MaasError): 28 | def __init__(self, data): 29 | self.message = "Invalid UUID - {0}".format(data) 30 | super(InvalidUuidFormatError, self).__init__(self.message) 31 | 32 | 33 | # In-case function parameter is optional but required 34 | class MissingFunctionParameter(MaasError): 35 | def __init__(self, data): 36 | self.message = "Missing parameter - {0}".format(data) 37 | super(MissingFunctionParameter, self).__init__(self.message) 38 | 39 | 40 | # In-case argument spec doesn't catch exception 41 | class MissingValueAnsible(MaasError): 42 | def __init__(self, data): 43 | self.message = "Missing value - {0}".format(data) 44 | super(MissingValueAnsible, self).__init__(self.message) 45 | 46 | 47 | # In-case MAAS API value is missing 48 | class MissingValueMAAS(MaasError): 49 | def __init__(self, data): 50 | self.message = "Missing value from MAAS API - {0}".format(data) 51 | super(MissingValueMAAS, self).__init__(self.message) 52 | 53 | 54 | class DeviceNotUnique(MaasError): 55 | def __init__(self, data): 56 | self.message = "Device is not unique - {0} - already exists".format( 57 | data 58 | ) 59 | super(DeviceNotUnique, self).__init__(self.message) 60 | 61 | 62 | class MachineNotFound(MaasError): 63 | def __init__(self, data): 64 | self.message = "Virtual machine - {0} - not found".format(data) 65 | super(MachineNotFound, self).__init__(self.message) 66 | 67 | 68 | class ClusterConnectionNotFound(MaasError): 69 | def __init__(self, data): 70 | self.message = "No cluster connection found - {0}".format(data) 71 | super(ClusterConnectionNotFound, self).__init__(self.message) 72 | 73 | 74 | class VlanNotFound(MaasError): 75 | def __init__(self, data): 76 | self.message = "VLAN - {0} - not found".format(data) 77 | super(VlanNotFound, self).__init__(self.message) 78 | 79 | 80 | class BlockDeviceNotFound(MaasError): 81 | def __init__(self, data): 82 | self.message = "Block device - {0} - not found".format(data) 83 | super(BlockDeviceNotFound, self).__init__(self.message) 84 | 85 | 86 | class PartitionNotFound(MaasError): 87 | def __init__(self, data): 88 | self.message = "Partition - {0} - not found".format(data) 89 | super(PartitionNotFound, self).__init__(self.message) 90 | -------------------------------------------------------------------------------- /tests/integration/targets/space/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | - name: List network spaces 11 | maas.maas.space_info: 12 | register: spaces 13 | - name: List network space 14 | maas.maas.space_info: 15 | name: "{{ spaces.records.0.name }}" 16 | register: space 17 | - ansible.builtin.assert: 18 | that: 19 | - space.records.0.name == spaces.records.0.name 20 | 21 | - name: Delete network space 22 | maas.maas.space: 23 | name: my-space 24 | state: absent 25 | 26 | - name: Delete network space 27 | maas.maas.space: 28 | name: my-space-updated 29 | state: absent 30 | 31 | - name: Add network space - missing parameter 32 | maas.maas.space: 33 | ignore_errors: true 34 | register: space 35 | - ansible.builtin.assert: 36 | that: 37 | - space is failed 38 | - "'missing required arguments: state' in space.msg" 39 | 40 | - name: Add network space - without name 41 | maas.maas.space: 42 | state: present 43 | description: my space 44 | register: space 45 | - ansible.builtin.assert: 46 | that: 47 | - space is changed 48 | # - space.record.description == "my space" 49 | 50 | - name: Delete network space 51 | maas.maas.space: 52 | name: "{{ space.record.name }}" 53 | state: absent 54 | - ansible.builtin.assert: 55 | that: 56 | - space is changed 57 | 58 | - name: Add network space - with name 59 | maas.maas.space: 60 | state: present 61 | name: my-space 62 | description: my space 63 | register: space 64 | - ansible.builtin.assert: 65 | that: 66 | - space is changed 67 | - space.record.name == "my-space" 68 | 69 | - name: Update network space 70 | maas.maas.space: 71 | state: present 72 | name: my-space 73 | new_name: my-space-updated 74 | description: my space updated 75 | register: space 76 | - ansible.builtin.assert: 77 | that: 78 | - space is changed 79 | - space.record.name == "my-space-updated" 80 | # - space.record.description == "my space updated" # description isn't returned 81 | 82 | - name: Update network space - idempotence 83 | maas.maas.space: 84 | state: present 85 | name: my-space-updated 86 | new_name: my-space-updated 87 | # description isn't returned so it can't be compared 88 | # description: my space updated 89 | register: space 90 | - ansible.builtin.assert: 91 | that: 92 | - space is not changed 93 | - space.record.name == "my-space-updated" 94 | # - space.record.description == "my space updated" # description isn't returned 95 | 96 | - name: Delete network space 97 | maas.maas.space: 98 | name: my-space-updated 99 | state: absent 100 | -------------------------------------------------------------------------------- /plugins/modules/subnet_ip_range_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: subnet_ip_range_info 13 | 14 | author: 15 | - Jure Medvesek (@juremedvesek) 16 | short_description: List IP ranges. 17 | description: 18 | - Plugin returns all IP ranges. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: {} 24 | """ 25 | 26 | EXAMPLES = r""" 27 | - name: List IP ranges 28 | maas.maas.subnet_ip_range_info: 29 | cluster_instance: 30 | host: ... 31 | token_key: ... 32 | token_secret: ... 33 | customer_key: ... 34 | """ 35 | 36 | RETURN = r""" 37 | record: 38 | description: 39 | - List IP ranges. 40 | returned: success 41 | type: list 42 | sample: 43 | - comment: '' 44 | end_ip: 10.10.10.254 45 | id: 1 46 | resource_uri: /MAAS/api/2.0/ipranges/1/ 47 | start_ip: 10.10.10.200 48 | subnet: 49 | active_discovery: false 50 | allow_dns: true 51 | allow_proxy: true 52 | cidr: 10.10.10.0/24 53 | description: '' 54 | disabled_boot_architectures: [] 55 | dns_servers: [] 56 | gateway_ip: 10.10.10.1 57 | id: 2 58 | managed: true 59 | name: 10.10.10.0/24 60 | rdns_mode: 2 61 | resource_uri: /MAAS/api/2.0/subnets/2/ 62 | space: undefined 63 | vlan: 64 | dhcp_on: true 65 | external_dhcp: null 66 | fabric: fabric-1 67 | fabric_id: 1 68 | id: 5002 69 | mtu: 1500 70 | name: untagged 71 | primary_rack: kwxmgm 72 | relay_vlan: null 73 | resource_uri: /MAAS/api/2.0/vlans/5002/ 74 | secondary_rack: null 75 | space: undefined 76 | vid: 0 77 | type: dynamic 78 | user: 79 | email: admin 80 | is_local: true 81 | is_superuser: true 82 | resource_uri: /MAAS/api/2.0/users/admin/ 83 | username: admin 84 | """ 85 | 86 | 87 | from ansible.module_utils.basic import AnsibleModule 88 | 89 | from ..module_utils import arguments, errors 90 | from ..module_utils.client import Client 91 | from ..module_utils.cluster_instance import get_oauth1_client 92 | 93 | 94 | def run(client: Client): 95 | response = client.get("/api/2.0/ipranges/") 96 | return response.json 97 | 98 | 99 | def main(): 100 | module = AnsibleModule( 101 | supports_check_mode=True, 102 | argument_spec=dict( 103 | arguments.get_spec("cluster_instance"), 104 | ), 105 | ) 106 | 107 | try: 108 | client = get_oauth1_client(module.params) 109 | records = run(client) 110 | module.exit_json(changed=False, records=records) 111 | except errors.MaasError as ex: 112 | module.fail_json(msg=str(ex)) 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /plugins/modules/fabric_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: fabric_info 13 | 14 | author: 15 | - Polona Mihalič (@PolonaM) 16 | short_description: Returns info about network fabrics. 17 | description: 18 | - Plugin returns information about all network fabrics or specific network fabric if I(name) is provided. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: 24 | name: 25 | description: 26 | - Name of the network fabric to be listed. 27 | - Serves as unique identifier of the network fabric. 28 | type: str 29 | """ 30 | 31 | EXAMPLES = r""" 32 | - name: Get list of all network fabrics 33 | maas.maas.fabric_info: 34 | cluster_instance: 35 | host: host-ip 36 | token_key: token-key 37 | token_secret: token-secret 38 | customer_key: customer-key 39 | 40 | - name: Get info about a specific network fabric 41 | maas.maas.fabric_info: 42 | cluster_instance: 43 | host: host-ip 44 | token_key: token-key 45 | token_secret: token-secret 46 | customer_key: customer-key 47 | name: my-network-fabric 48 | """ 49 | 50 | RETURN = r""" 51 | records: 52 | description: 53 | - Network fabric info list. 54 | returned: success 55 | type: list 56 | sample: 57 | class_type: null 58 | id: 7 59 | name: fabric-7 60 | resource_uri: /MAAS/api/2.0/fabrics/7/ 61 | vlans: 62 | - dhcp_on: false 63 | external_dhcp: null 64 | fabric: fabric-0 65 | fabric_id: 0 66 | id: 5001 67 | mtu: 1500 68 | name: untagged 69 | primary_rack: null 70 | relay_vlan: null 71 | resource_uri: /MAAS/api/2.0/vlans/5001/ 72 | secondary_rack: null 73 | space: undefined 74 | vid: 0 75 | """ 76 | 77 | 78 | from ansible.module_utils.basic import AnsibleModule 79 | 80 | from ..module_utils import arguments, errors 81 | from ..module_utils.client import Client 82 | from ..module_utils.cluster_instance import get_oauth1_client 83 | from ..module_utils.fabric import Fabric 84 | 85 | 86 | def run(module, client: Client): 87 | if module.params["name"]: 88 | fabric = Fabric.get_by_name(module, client, must_exist=True) 89 | response = [fabric.to_ansible()] 90 | else: 91 | response = client.get("/api/2.0/fabrics/").json 92 | return response 93 | 94 | 95 | def main(): 96 | module = AnsibleModule( 97 | supports_check_mode=True, 98 | argument_spec=dict( 99 | arguments.get_spec("cluster_instance"), 100 | name=dict(type="str"), 101 | ), 102 | ) 103 | 104 | try: 105 | client = get_oauth1_client(module.params) 106 | records = run(module, client) 107 | module.exit_json(changed=False, records=records) 108 | except errors.MaasError as e: 109 | module.fail_json(msg=str(e)) 110 | 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /plugins/module_utils/space.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | from ..module_utils import errors 12 | from ..module_utils.rest_client import RestClient 13 | from ..module_utils.utils import MaasValueMapper, get_query 14 | 15 | 16 | class Space(MaasValueMapper): 17 | def __init__( 18 | self, 19 | name=None, 20 | id=None, 21 | vlans=None, 22 | resource_uri=None, 23 | subnets=None, 24 | ): 25 | self.name = name 26 | self.id = id 27 | self.vlans = vlans 28 | self.resource_uri = resource_uri 29 | self.subnets = subnets 30 | 31 | @classmethod 32 | def get_by_name( 33 | cls, module, client, must_exist=False, name_field_ansible="name" 34 | ): 35 | rest_client = RestClient(client=client) 36 | query = get_query( 37 | module, 38 | name_field_ansible, 39 | ansible_maas_map={name_field_ansible: "name"}, 40 | ) 41 | space_maas_dict = rest_client.get_record( 42 | "/api/2.0/spaces/", query, must_exist=must_exist 43 | ) 44 | if space_maas_dict: 45 | space = cls.from_maas(space_maas_dict) 46 | return space 47 | 48 | @classmethod 49 | def from_ansible(cls, module): 50 | return 51 | 52 | @classmethod 53 | def from_maas(cls, maas_dict): 54 | obj = cls() 55 | try: 56 | obj.name = maas_dict["name"] 57 | obj.id = maas_dict["id"] 58 | obj.vlans = maas_dict["vlans"] 59 | obj.resource_uri = maas_dict["resource_uri"] 60 | obj.subnets = maas_dict["subnets"] 61 | except KeyError as e: 62 | raise errors.MissingValueMAAS(e) 63 | return obj 64 | 65 | def to_maas(self): 66 | return 67 | 68 | def to_ansible(self): 69 | return dict( 70 | name=self.name, 71 | id=self.id, 72 | resource_uri=self.resource_uri, 73 | vlans=self.vlans, 74 | subnets=self.subnets, 75 | ) 76 | 77 | def delete(self, client): 78 | client.delete(f"/api/2.0/spaces/{self.id}/") 79 | 80 | def update(self, client, payload): 81 | return client.put(f"/api/2.0/spaces/{self.id}/", data=payload).json 82 | 83 | @classmethod 84 | def create(cls, client, payload): 85 | space_maas_dict = client.post( 86 | "/api/2.0/spaces/", 87 | data=payload, 88 | timeout=60, # Sometimes we get timeout error thus changing timeout from 20s to 60s 89 | ).json 90 | space = cls.from_maas(space_maas_dict) 91 | return space 92 | 93 | def __eq__(self, other): 94 | """One space is equal to another if it has all attributes exactly the same""" 95 | return all( 96 | ( 97 | self.name == other.name, 98 | self.id == other.id, 99 | self.vlans == other.vlans, 100 | self.resource_uri == other.resource_uri, 101 | self.subnets == other.subnets, 102 | ) 103 | ) 104 | -------------------------------------------------------------------------------- /plugins/modules/user_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: user_info 13 | 14 | author: 15 | - Domen Dobnikar (@domen_dobnikar) 16 | short_description: Get information about user accounts. 17 | description: Get information about all or specific user. 18 | version_added: 1.0.0 19 | extends_documentation_fragment: 20 | - maas.maas.cluster_instance 21 | seealso: [] 22 | options: 23 | name: 24 | description: 25 | - The user name. 26 | - Identifier-style username for the user. 27 | type: str 28 | """ 29 | 30 | EXAMPLES = r""" 31 | - name: List account information about all users 32 | maas.maas.user_info: 33 | cluster_instance: 34 | host: host-ip 35 | token_key: token-key 36 | token_secret: token-secret 37 | customer_key: customer-key 38 | register: users 39 | - ansible.builtin.debug: 40 | var: users 41 | 42 | - name: List account information about a specific user 43 | maas.maas.user_info: 44 | cluster_instance: 45 | host: host-ip 46 | token_key: token-key 47 | token_secret: token-secret 48 | customer_key: customer-key 49 | name: some_username 50 | register: user 51 | - ansible.builtin.debug: 52 | var: user 53 | """ 54 | 55 | RETURN = r""" 56 | record: 57 | description: 58 | - Users account information 59 | returned: success 60 | type: dict 61 | sample: 62 | - email: maas@localhost 63 | is_local: true 64 | is_superuser: false 65 | resource_uri: /MAAS/api/2.0/users/MAAS/ 66 | username: MAAS 67 | - email: admin 68 | is_local: true 69 | is_superuser: true 70 | resource_uri: /MAAS/api/2.0/users/admin/ 71 | username: admin 72 | - email: node-init-user@localhost 73 | is_local: true 74 | is_superuser: false 75 | resource_uri: /MAAS/api/2.0/users/maas-init-node/ 76 | username: maas-init-node 77 | """ 78 | 79 | from ansible.module_utils.basic import AnsibleModule 80 | 81 | from ..module_utils import arguments, errors 82 | from ..module_utils.cluster_instance import get_oauth1_client 83 | 84 | 85 | def run(module, client): 86 | if module.params["name"]: 87 | response = client.get(f"/api/2.0/users/{module.params['name']}/") 88 | if response.status == 404: 89 | module.warn(f"User - {module.params['name']} - does not exist.") 90 | return None 91 | else: 92 | response = client.get("/api/2.0/users/") 93 | return response.json 94 | 95 | 96 | def main(): 97 | module = AnsibleModule( 98 | supports_check_mode=True, 99 | argument_spec=dict( 100 | arguments.get_spec("cluster_instance"), 101 | name=dict( 102 | type="str", 103 | ), 104 | ), 105 | ) 106 | 107 | try: 108 | client = get_oauth1_client(module.params) 109 | record = run(module, client) 110 | module.exit_json(changed=False, record=record) 111 | except errors.MaasError as e: 112 | module.fail_json(msg=str(e)) 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /plugins/module_utils/fabric.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | from ..module_utils import errors 12 | from ..module_utils.rest_client import RestClient 13 | from ..module_utils.utils import MaasValueMapper, get_query 14 | 15 | 16 | class Fabric(MaasValueMapper): 17 | def __init__( 18 | self, 19 | name=None, 20 | id=None, 21 | vlans=None, 22 | resource_uri=None, 23 | class_type=None, 24 | ): 25 | self.name = name 26 | self.id = id 27 | self.vlans = vlans 28 | self.resource_uri = resource_uri 29 | self.class_type = class_type 30 | 31 | @classmethod 32 | def get_by_name( 33 | cls, module, client, must_exist=False, name_field_ansible="name" 34 | ): 35 | rest_client = RestClient(client=client) 36 | query = get_query( 37 | module, 38 | name_field_ansible, 39 | ansible_maas_map={name_field_ansible: "name"}, 40 | ) 41 | fabric_maas_dict = rest_client.get_record( 42 | "/api/2.0/fabrics/", query, must_exist=must_exist 43 | ) 44 | if fabric_maas_dict: 45 | fabric = cls.from_maas(fabric_maas_dict) 46 | return fabric 47 | 48 | @classmethod 49 | def from_ansible(cls, module): 50 | return 51 | 52 | @classmethod 53 | def from_maas(cls, maas_dict): 54 | obj = cls() 55 | try: 56 | obj.name = maas_dict["name"] 57 | obj.id = maas_dict["id"] 58 | obj.vlans = maas_dict["vlans"] 59 | obj.resource_uri = maas_dict["resource_uri"] 60 | obj.class_type = maas_dict["class_type"] 61 | except KeyError as e: 62 | raise errors.MissingValueMAAS(e) 63 | return obj 64 | 65 | def to_maas(self): 66 | return 67 | 68 | def to_ansible(self): 69 | return dict( 70 | name=self.name, 71 | id=self.id, 72 | resource_uri=self.resource_uri, 73 | vlans=self.vlans, 74 | class_type=self.class_type, 75 | ) 76 | 77 | def delete(self, client): 78 | client.delete(f"/api/2.0/fabrics/{self.id}/") 79 | 80 | def update(self, client, payload): 81 | return client.put(f"/api/2.0/fabrics/{self.id}/", data=payload).json 82 | 83 | @classmethod 84 | def create(cls, client, payload): 85 | fabric_maas_dict = client.post( 86 | "/api/2.0/fabrics/", 87 | data=payload, 88 | timeout=60, # Sometimes we get timeout error thus changing timeout from 20s to 60s 89 | ).json 90 | fabric = cls.from_maas(fabric_maas_dict) 91 | return fabric 92 | 93 | def __eq__(self, other): 94 | """One fabric is equal to another if it has all attributes exactly the same""" 95 | return all( 96 | ( 97 | self.name == other.name, 98 | self.id == other.id, 99 | self.vlans == other.vlans, 100 | self.resource_uri == other.resource_uri, 101 | self.class_type == other.class_type, 102 | ) 103 | ) 104 | -------------------------------------------------------------------------------- /tests/unit/plugins/modules/test_space.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils.space import Space 15 | from ansible_collections.maas.maas.plugins.modules import space 16 | 17 | pytestmark = pytest.mark.skipif( 18 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 19 | ) 20 | 21 | 22 | class TestMain: 23 | def test_all_params(self, run_main): 24 | params = dict( 25 | cluster_instance=dict( 26 | host="https://0.0.0.0", 27 | token_key="URCfn6EhdZ", 28 | token_secret="PhXz3ncACvkcK", 29 | customer_key="nzW4EBWjyDe", 30 | ), 31 | name="my-space", 32 | state="present", 33 | new_name="updated-space", 34 | description="Updated Network Space", 35 | ) 36 | 37 | success, result = run_main(space, params) 38 | 39 | assert success is True 40 | 41 | def test_minimal_set_of_params(self, run_main): 42 | params = dict( 43 | cluster_instance=dict( 44 | host="https://0.0.0.0", 45 | token_key="URCfn6EhdZ", 46 | token_secret="PhXz3ncACvkcK", 47 | customer_key="nzW4EBWjyDe", 48 | ), 49 | state="present", 50 | ) 51 | 52 | success, result = run_main(space, params) 53 | 54 | assert success is True 55 | 56 | def test_fail(self, run_main): 57 | success, result = run_main(space) 58 | 59 | assert success is False 60 | assert "missing required arguments: state" in result["msg"] 61 | 62 | 63 | class TestDataForCreateSpace: 64 | def test_data_for_create_space(self, create_module): 65 | module = create_module( 66 | params=dict( 67 | cluster_instance=dict( 68 | host="https://0.0.0.0", 69 | token_key="URCfn6EhdZ", 70 | token_secret="PhXz3ncACvkcK", 71 | customer_key="nzW4EBWjyDe", 72 | ), 73 | state="present", 74 | name="my-space", 75 | new_name=None, 76 | description=None, 77 | ) 78 | ) 79 | data = space.data_for_create_space(module) 80 | 81 | assert data == dict(name="my-space") 82 | 83 | 84 | class TestDataForUpdateSpace: 85 | def test_data_for_update_space(self, create_module): 86 | module = create_module( 87 | params=dict( 88 | cluster_instance=dict( 89 | host="https://0.0.0.0", 90 | token_key="URCfn6EhdZ", 91 | token_secret="PhXz3ncACvkcK", 92 | customer_key="nzW4EBWjyDe", 93 | ), 94 | state="present", 95 | name="old-name", 96 | new_name="new-name", 97 | description="description", 98 | ), 99 | ) 100 | old_space = Space(name="old-name") 101 | data = space.data_for_update_space(module, old_space) 102 | 103 | assert data == dict( 104 | description="description", 105 | name="new-name", 106 | ) 107 | -------------------------------------------------------------------------------- /tests/integration/targets/dns_record/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | vars: 8 | domain_name: test-domain-for-dns-records 9 | block: 10 | # Ensure that doamin exist 11 | - name: Create domain 12 | maas.maas.dns_domain: 13 | name: "{{ domain_name }}" 14 | state: present 15 | 16 | # CNAME record 17 | - name: Create CNAME record 18 | maas.maas.dns_record: 19 | name: "cname" 20 | domain: "{{ domain_name }}" 21 | state: present 22 | type: CNAME 23 | data: maas.io 24 | register: created 25 | 26 | - ansible.builtin.debug: 27 | var: created 28 | 29 | - ansible.builtin.assert: 30 | that: 31 | created.record.data == "maas.io" 32 | 33 | - name: Edit CNAME record 34 | maas.maas.dns_record: 35 | fqdn: "cname.{{ domain_name }}" 36 | state: present 37 | type: CNAME 38 | data: maas.io2 39 | register: edited 40 | 41 | - ansible.builtin.assert: 42 | that: 43 | edited.record.data == "maas.io2" 44 | 45 | - name: Change CNAME record to A/AAAA record - should fail 46 | maas.maas.dns_record: 47 | fqdn: "cname.{{ domain_name }}" 48 | state: present 49 | type: A/AAAA 50 | data: maas.a 51 | register: not_possible_transition 52 | failed_when: not_possible_transition is not failed 53 | 54 | - name: Remove CNAME record 55 | maas.maas.dns_record: 56 | name: "cname" 57 | domain: "{{ domain_name }}" 58 | state: absent 59 | register: delete_succed 60 | 61 | - ansible.builtin.assert: 62 | that: 63 | delete_succed is changed 64 | 65 | # A/AAAA record 66 | - name: Create A/AAAA record 67 | maas.maas.dns_record: 68 | fqdn: "aaaa.{{ domain_name }}" 69 | state: present 70 | type: A/AAAA 71 | data: 1.2.3.4 72 | register: created 73 | 74 | - ansible.builtin.debug: 75 | var: created 76 | 77 | - ansible.builtin.assert: 78 | that: 79 | created.record.data == "1.2.3.4" 80 | 81 | - name: Edit A/AAAA record 82 | maas.maas.dns_record: 83 | fqdn: "aaaa.{{ domain_name }}" 84 | state: present 85 | type: A/AAAA 86 | data: 1.2.3.4 1.2.3.6 87 | register: edited 88 | 89 | - ansible.builtin.assert: 90 | that: 91 | edited.record.data == "1.2.3.4 1.2.3.6" 92 | 93 | - name: Delete domain - should fail because not empty 94 | maas.maas.dns_domain: 95 | name: "{{ domain_name }}" 96 | state: absent 97 | register: not_empty_domain_deleted 98 | failed_when: not_empty_domain_deleted is not failed 99 | 100 | - name: Remove A/AAAA record 101 | maas.maas.dns_record: 102 | fqdn: "aaaa.{{ domain_name }}" 103 | state: absent 104 | 105 | always: 106 | # clean up environment after tests 107 | - name: Remove CNAME record 108 | maas.maas.dns_record: 109 | fqdn: "cname.{{ domain_name }}" 110 | state: absent 111 | 112 | - name: Remove A/AAAA record 113 | maas.maas.dns_record: 114 | fqdn: "aaaa.{{ domain_name }}" 115 | state: absent 116 | 117 | - name: Delete domain - to have clean environment after tests 118 | maas.maas.dns_domain: 119 | name: "{{ domain_name }}" 120 | state: absent 121 | -------------------------------------------------------------------------------- /tests/integration/targets/fabric/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | - name: List network fabrics 11 | maas.maas.fabric_info: 12 | register: fabrics 13 | - name: List network fabrics 14 | maas.maas.fabric_info: 15 | name: "{{ fabrics.records.0.name }}" 16 | register: fabric 17 | - ansible.builtin.assert: 18 | that: 19 | - fabric.records.0.name == fabrics.records.0.name 20 | 21 | - name: Delete network fabric 22 | maas.maas.fabric: 23 | name: my-fabric 24 | state: absent 25 | 26 | - name: Delete network fabric 27 | maas.maas.fabric: 28 | name: my-fabric-updated 29 | state: absent 30 | 31 | - name: Add network fabric - missing parameter 32 | maas.maas.fabric: 33 | ignore_errors: true 34 | register: fabric 35 | - ansible.builtin.assert: 36 | that: 37 | - fabric is failed 38 | - "'missing required arguments: state' in fabric.msg" 39 | 40 | - name: Add network fabric - without name 41 | maas.maas.fabric: 42 | state: present 43 | description: my fabric 44 | class_type: a 45 | register: fabric 46 | - ansible.builtin.assert: 47 | that: 48 | - fabric is changed 49 | - fabric.record.class_type == "a" 50 | # - fabric.record.description == "my fabric" 51 | 52 | - name: Delete network fabric 53 | maas.maas.fabric: 54 | name: "{{ fabric.record.name }}" 55 | state: absent 56 | - ansible.builtin.assert: 57 | that: 58 | - fabric is changed 59 | 60 | - name: Add network fabric - with name 61 | maas.maas.fabric: 62 | state: present 63 | name: my-fabric 64 | description: my fabric 65 | class_type: a 66 | register: fabric 67 | - ansible.builtin.assert: 68 | that: 69 | - fabric is changed 70 | - fabric.record.name == "my-fabric" 71 | - fabric.record.class_type == "a" 72 | # - fabric.record.description == "my fabric" 73 | 74 | - name: Update network fabric 75 | maas.maas.fabric: 76 | state: present 77 | name: my-fabric 78 | new_name: my-fabric-updated 79 | description: my fabric updated 80 | class_type: b 81 | register: fabric 82 | - ansible.builtin.assert: 83 | that: 84 | - fabric is changed 85 | - fabric.record.name == "my-fabric-updated" 86 | - fabric.record.class_type == "b" 87 | # - fabric.record.description == "my fabric updated" # description isn't returned 88 | 89 | - name: Update network fabric - idempotence 90 | maas.maas.fabric: 91 | state: present 92 | name: my-fabric-updated 93 | new_name: my-fabric-updated 94 | class_type: b 95 | # description isn't returned so it can't be compared 96 | # description: my fabric updated 97 | register: fabric 98 | - ansible.builtin.assert: 99 | that: 100 | - fabric is not changed 101 | - fabric.record.name == "my-fabric-updated" 102 | - fabric.record.class_type == "b" 103 | # - fabric.record.description == "my fabric updated" # description isn't returned 104 | 105 | - name: Delete network fabric 106 | maas.maas.fabric: 107 | name: my-fabric-updated 108 | state: absent 109 | -------------------------------------------------------------------------------- /tests/unit/plugins/modules/test_fabric.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils.fabric import Fabric 15 | from ansible_collections.maas.maas.plugins.modules import fabric 16 | 17 | pytestmark = pytest.mark.skipif( 18 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 19 | ) 20 | 21 | 22 | class TestMain: 23 | def test_all_params(self, run_main): 24 | params = dict( 25 | cluster_instance=dict( 26 | host="https://0.0.0.0", 27 | token_key="URCfn6EhdZ", 28 | token_secret="PhXz3ncACvkcK", 29 | customer_key="nzW4EBWjyDe", 30 | ), 31 | name="my-fabric", 32 | state="present", 33 | new_name="updated-fabric", 34 | description="Updated Network Fabric", 35 | ) 36 | 37 | success, result = run_main(fabric, params) 38 | 39 | assert success is True 40 | 41 | def test_minimal_set_of_params(self, run_main): 42 | params = dict( 43 | cluster_instance=dict( 44 | host="https://0.0.0.0", 45 | token_key="URCfn6EhdZ", 46 | token_secret="PhXz3ncACvkcK", 47 | customer_key="nzW4EBWjyDe", 48 | ), 49 | state="present", 50 | ) 51 | 52 | success, result = run_main(fabric, params) 53 | 54 | assert success is True 55 | 56 | def test_fail(self, run_main): 57 | success, result = run_main(fabric) 58 | 59 | assert success is False 60 | assert "missing required arguments: state" in result["msg"] 61 | 62 | 63 | class TestDataForCreateFabric: 64 | def test_data_for_create_fabric(self, create_module): 65 | module = create_module( 66 | params=dict( 67 | cluster_instance=dict( 68 | host="https://0.0.0.0", 69 | token_key="URCfn6EhdZ", 70 | token_secret="PhXz3ncACvkcK", 71 | customer_key="nzW4EBWjyDe", 72 | ), 73 | state="present", 74 | name="my-fabric", 75 | new_name=None, 76 | description=None, 77 | class_type=None, 78 | ) 79 | ) 80 | data = fabric.data_for_create_fabric(module) 81 | 82 | assert data == dict(name="my-fabric") 83 | 84 | 85 | class TestDataForUpdateFabric: 86 | def test_data_for_update_fabric(self, create_module): 87 | module = create_module( 88 | params=dict( 89 | cluster_instance=dict( 90 | host="https://0.0.0.0", 91 | token_key="URCfn6EhdZ", 92 | token_secret="PhXz3ncACvkcK", 93 | customer_key="nzW4EBWjyDe", 94 | ), 95 | state="present", 96 | name="old-name", 97 | new_name="new-name", 98 | description="description", 99 | class_type="class_type", 100 | ), 101 | ) 102 | old_fabric = Fabric(name="old-name") 103 | data = fabric.data_for_update_fabric(module, old_fabric) 104 | 105 | assert data == dict( 106 | description="description", 107 | name="new-name", 108 | class_type="class_type", 109 | ) 110 | -------------------------------------------------------------------------------- /plugins/module_utils/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | from ..module_utils import errors 12 | from ..module_utils.rest_client import RestClient 13 | from ..module_utils.utils import MaasValueMapper, get_query 14 | 15 | __metaclass__ = type 16 | 17 | 18 | class User(MaasValueMapper): 19 | def __init__( 20 | # Add more values as needed. 21 | self, 22 | is_admin=None, 23 | name=None, 24 | email=None, 25 | is_local=None, 26 | password=None, 27 | ): 28 | self.is_admin = is_admin 29 | self.name = name 30 | self.email = email 31 | self.is_local = is_local 32 | self.password = password 33 | 34 | def __eq__(self, other): 35 | return self.to_ansible() == other.to_ansible() 36 | 37 | @classmethod 38 | def from_ansible(cls, module): 39 | obj = User() 40 | obj.name = module.get("name") 41 | obj.email = module.get("email") 42 | obj.is_admin = module.get("is_admin") 43 | obj.password = module.get("password") 44 | return obj 45 | 46 | @classmethod 47 | def from_maas(cls, maas_dict): 48 | obj = User() 49 | try: 50 | obj.is_admin = maas_dict["is_superuser"] 51 | obj.email = maas_dict["email"] 52 | obj.name = maas_dict["username"] 53 | obj.is_local = maas_dict["is_local"] 54 | except KeyError as e: 55 | raise errors.MissingValueMAAS(e) 56 | return obj 57 | 58 | @classmethod 59 | def get_by_name( 60 | cls, module, client, must_exist=False, name_field_ansible="name" 61 | ): 62 | # Returns machine object or None 63 | rest_client = RestClient(client=client) 64 | query = get_query( 65 | module, 66 | name_field_ansible, 67 | ansible_maas_map={name_field_ansible: "username"}, 68 | ) 69 | maas_dict = rest_client.get_record( 70 | "/api/2.0/users/", 71 | query, 72 | must_exist=must_exist, 73 | ) 74 | if maas_dict: 75 | user_from_maas = cls.from_maas(maas_dict) 76 | return user_from_maas 77 | 78 | def to_maas(self): 79 | to_maas_dict = {} 80 | if self.name: 81 | to_maas_dict["username"] = self.name 82 | if self.email: 83 | to_maas_dict["email"] = self.email 84 | if self.is_admin is not None: 85 | to_maas_dict["is_superuser"] = self.is_admin 86 | if self.is_local: 87 | to_maas_dict["is_local"] = self.is_local 88 | if self.password: 89 | to_maas_dict["password"] = self.password 90 | return to_maas_dict 91 | 92 | def to_ansible(self): 93 | return dict( 94 | name=self.name, 95 | email=self.email, 96 | is_admin=self.is_admin, 97 | is_local=self.is_local, 98 | ) 99 | 100 | def payload_for_create(self): 101 | payload = self.to_maas() 102 | if payload["is_superuser"]: 103 | payload["is_superuser"] = 1 104 | else: 105 | payload["is_superuser"] = 0 106 | return payload 107 | 108 | def send_create_request(self, client, payload): 109 | results = client.post( 110 | "/api/2.0/users/", 111 | data=payload, 112 | ).json 113 | return results 114 | 115 | def send_delete_request(self, client): 116 | client.delete(f"/api/2.0/users/{self.name}/") 117 | -------------------------------------------------------------------------------- /plugins/modules/subnet_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: subnet_info 13 | 14 | author: 15 | - Jure Medvesek (@juremedvesek) 16 | short_description: List subnets. 17 | description: 18 | - Plugin returns all subnets. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: {} 24 | """ 25 | 26 | EXAMPLES = r""" 27 | - name: List subnets 28 | maas.maas.subnet_info: 29 | cluster_instance: 30 | host: ... 31 | token_key: ... 32 | token_secret: ... 33 | customer_key: ... 34 | """ 35 | 36 | RETURN = r""" 37 | record: 38 | description: 39 | - List subnets. 40 | returned: success 41 | type: list 42 | sample: 43 | records: 44 | - active_discovery: false 45 | allow_dns: true 46 | allow_proxy: true 47 | cidr: 10.157.248.0/24 48 | description: '' 49 | disabled_boot_architectures: [] 50 | dns_servers: [] 51 | gateway_ip: 10.157.248.1 52 | id: 1 53 | ip_ranges: 54 | - end_ip: 192.168.0.128 55 | start_ip: 192.168.0.64 56 | type: reserved 57 | managed: true 58 | name: 10.157.248.0/24 59 | rdns_mode: 2 60 | resource_uri: /MAAS/api/2.0/subnets/1/ 61 | space: undefined 62 | vlan: 63 | dhcp_on: false 64 | external_dhcp: null 65 | fabric: fabric-0 66 | fabric_id: 0 67 | id: 5001 68 | mtu: 1500 69 | name: untagged 70 | primary_rack: null 71 | relay_vlan: null 72 | resource_uri: /MAAS/api/2.0/vlans/5001/ 73 | secondary_rack: null 74 | space: undefined 75 | vid: 0 76 | """ 77 | 78 | from itertools import groupby 79 | 80 | from ansible.module_utils.basic import AnsibleModule 81 | 82 | from ..module_utils import arguments, errors 83 | from ..module_utils.client import Client 84 | from ..module_utils.cluster_instance import get_oauth1_client 85 | 86 | 87 | def get_ip_ranges(client): 88 | def key_function(item): 89 | return item["subnet"] 90 | 91 | ip_ranges = client.get("/api/2.0/ipranges/").json 92 | data = [ 93 | { 94 | "subnet": ip_range["subnet"]["name"], 95 | "data": { 96 | "type": ip_range["type"], 97 | "start_ip": ip_range["start_ip"], 98 | "end_ip": ip_range["end_ip"], 99 | }, 100 | } 101 | for ip_range in ip_ranges 102 | ] 103 | 104 | sorted_data = sorted(data, key=key_function) 105 | 106 | grouped = { 107 | k: [v["data"] for v in g] 108 | for k, g in groupby(sorted_data, key_function) 109 | } 110 | return grouped 111 | 112 | 113 | def run(client: Client): 114 | subnets = client.get("/api/2.0/subnets/").json 115 | ip_ranges = get_ip_ranges(client) 116 | for subnet in subnets: 117 | subnet["ip_ranges"] = ip_ranges.get(subnet["name"], []) 118 | 119 | return subnets 120 | 121 | 122 | def main(): 123 | module = AnsibleModule( 124 | supports_check_mode=True, 125 | argument_spec=dict( 126 | arguments.get_spec("cluster_instance"), 127 | ), 128 | ) 129 | 130 | try: 131 | client = get_oauth1_client(module.params) 132 | records = run(client) 133 | module.exit_json(changed=False, records=records) 134 | except errors.MaasError as ex: 135 | module.fail_json(msg=str(ex)) 136 | 137 | 138 | if __name__ == "__main__": 139 | main() 140 | -------------------------------------------------------------------------------- /plugins/module_utils/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | from abc import abstractmethod 9 | 10 | from ..module_utils import errors 11 | 12 | __metaclass__ = type 13 | 14 | 15 | class MaasValueMapper: 16 | """ 17 | Represent abstract class. 18 | """ 19 | 20 | @abstractmethod 21 | def to_ansible(self): 22 | """ 23 | Transforms from python-native to ansible-native object. 24 | :return: ansible-native dictionary. 25 | """ 26 | pass 27 | 28 | @abstractmethod 29 | def to_maas(self): 30 | """ 31 | Transforms python-native to maas-native object. 32 | :return: maas-native dictionary. 33 | """ 34 | pass 35 | 36 | @classmethod 37 | @abstractmethod 38 | def from_ansible(cls, module): 39 | """ 40 | Transforms from ansible_data (module.params) to python-object. 41 | :param ansible_data: Field that is inputed from ansible playbook. Is most likely 42 | equivalent to "module.params" in python 43 | :return: python object 44 | """ 45 | pass 46 | 47 | @classmethod 48 | @abstractmethod 49 | def from_maas(cls, maas_dict): 50 | """ 51 | Transforms from maas-native dictionary to python-object. 52 | :param maas_dict: Dictionary from maas API 53 | :return: python object 54 | """ 55 | pass 56 | 57 | 58 | def filter_dict(input, *field_names): 59 | output = {} 60 | for field_name in field_names: 61 | if field_name not in input: 62 | continue 63 | value = input[field_name] 64 | if value is not None: 65 | output[field_name] = value 66 | return output 67 | 68 | 69 | def is_superset(superset, subset): 70 | if not subset: 71 | return True 72 | for k, v in subset.items(): 73 | if k in superset and superset[k] == v: 74 | continue 75 | return False 76 | return True 77 | 78 | 79 | def filter_results(results, filter_data): 80 | return [ 81 | element for element in results if is_superset(element, filter_data) 82 | ] 83 | 84 | 85 | def get_query(module, *field_names, ansible_maas_map): 86 | """ 87 | Wrapps filter_dict and transform_ansible_to_maas_query. Prefer to use 'get_query' over filter_dict 88 | even if there's no mapping between maas and ansible columns for the sake of verbosity and consistency 89 | """ 90 | ansible_query = filter_dict(module.params, *field_names) 91 | maas_query = transform_query(ansible_query, ansible_maas_map) 92 | return maas_query 93 | 94 | 95 | def transform_query(raw_query, query_map): 96 | # Transforms query by renaming raw_query's keys by specifying those keys and the new values in query_map 97 | return {query_map[key]: raw_query[key] for key, value in raw_query.items()} 98 | 99 | 100 | def is_changed(before, after): 101 | return not before == after 102 | 103 | 104 | def required_one_of(module, option, list_suboptions): 105 | # This enables to check suboptions of an option in ansible module. 106 | # Fails playbook if all suboptions are missing. 107 | if module.params[option] is None: 108 | return 109 | module_suboptions = module.params[option].keys() 110 | for suboption in list_suboptions: 111 | if ( 112 | suboption in module_suboptions 113 | and module.params[option][suboption] is not None 114 | ): 115 | return 116 | raise errors.MaasError( 117 | f"{option}: at least one of the options is required: {list_suboptions}" 118 | ) 119 | -------------------------------------------------------------------------------- /plugins/module_utils/vmhost.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | from ..module_utils import errors 12 | from ..module_utils.rest_client import RestClient 13 | from ..module_utils.utils import MaasValueMapper, get_query 14 | 15 | 16 | class VMHost(MaasValueMapper): 17 | def __init__( 18 | # Add more values as needed. 19 | self, 20 | name=None, # Host name. 21 | id=None, 22 | cpu_over_commit_ratio=None, 23 | memory_over_commit_ratio=None, 24 | default_macvlan_mode=None, 25 | pool=None, 26 | zone=None, 27 | tags=None, 28 | ): 29 | self.name = name 30 | self.id = id 31 | self.cpu_over_commit_ratio = cpu_over_commit_ratio 32 | self.memory_over_commit_ratio = memory_over_commit_ratio 33 | self.default_macvlan_mode = default_macvlan_mode 34 | self.pool = pool 35 | self.zone = zone 36 | self.tags = tags 37 | 38 | @classmethod 39 | def get_by_name( 40 | cls, module, client, must_exist=False, name_field_ansible="name" 41 | ): 42 | rest_client = RestClient(client=client) 43 | query = get_query( 44 | module, 45 | name_field_ansible, 46 | ansible_maas_map={name_field_ansible: "name"}, 47 | ) 48 | maas_dict = rest_client.get_record( 49 | "/api/2.0/vm-hosts/", query, must_exist=must_exist 50 | ) 51 | if maas_dict: 52 | vmhost_from_maas = cls.from_maas(maas_dict) 53 | return vmhost_from_maas 54 | 55 | @classmethod 56 | def from_ansible(cls, module): 57 | return 58 | 59 | @classmethod 60 | def from_maas(cls, maas_dict): 61 | obj = cls() 62 | try: 63 | obj.name = maas_dict["name"] 64 | obj.id = maas_dict["id"] 65 | obj.cpu_over_commit_ratio = maas_dict["cpu_over_commit_ratio"] 66 | obj.memory_over_commit_ratio = maas_dict[ 67 | "memory_over_commit_ratio" 68 | ] 69 | obj.default_macvlan_mode = maas_dict["default_macvlan_mode"] 70 | obj.tags = maas_dict["tags"] 71 | obj.zone = maas_dict["zone"] 72 | obj.pool = maas_dict["pool"] 73 | except KeyError as e: 74 | raise errors.MissingValueMAAS(e) 75 | return obj 76 | 77 | def to_maas(self): 78 | return 79 | 80 | def to_ansible(self): 81 | return 82 | 83 | def send_compose_request(self, module, client, payload): 84 | results = client.post( 85 | f"/api/2.0/vm-hosts/{self.id}/", 86 | query={"op": "compose"}, 87 | data=payload, 88 | ).json 89 | return results 90 | 91 | def delete(self, client): 92 | client.delete(f"/api/2.0/vm-hosts/{self.id}/") 93 | 94 | # Used because we don't store all the data in an object 95 | def get(self, client): 96 | return client.get(f"/api/2.0/vm-hosts/{self.id}/").json 97 | 98 | @classmethod 99 | def create(cls, client, payload, timeout=60): 100 | vm_host_maas_dict = client.post( 101 | "/api/2.0/vm-hosts/", 102 | data=payload, 103 | timeout=timeout, 104 | ).json 105 | vm_host = cls.from_maas(vm_host_maas_dict) 106 | return vm_host, vm_host_maas_dict 107 | 108 | def update(self, client, payload, timeout=60): 109 | return client.put( 110 | f"/api/2.0/vm-hosts/{self.id}/", 111 | data=payload, 112 | timeout=timeout, 113 | ).json 114 | -------------------------------------------------------------------------------- /plugins/module_utils/rest_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2021, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | from . import errors, utils 9 | 10 | __metaclass__ = type 11 | 12 | 13 | def _query(original=None): 14 | # Make sure the query isn't equal to None 15 | # If any default query values need to added in the future, they may be added here 16 | return dict(original or {}) 17 | 18 | 19 | class RestClient: 20 | def __init__(self, client): 21 | self.client = client 22 | 23 | def list_records(self, endpoint, query=None, timeout=120): 24 | """Results are obtained so that first off, all records are obtained and 25 | then filtered manually""" 26 | try: 27 | records = self.client.get(path=endpoint, timeout=timeout).json 28 | except TimeoutError as e: 29 | raise errors.MaasError(f"Request timed out: {e}") 30 | return utils.filter_results(records, query) 31 | 32 | def get_record(self, endpoint, query=None, must_exist=False, timeout=120): 33 | records = self.list_records( 34 | endpoint=endpoint, query=query, timeout=timeout 35 | ) 36 | if len(records) > 1: 37 | raise errors.MaasError( 38 | "{0} records from endpoint {1} match the {2} query.".format( 39 | len(records), endpoint, query 40 | ) 41 | ) 42 | if must_exist and not records: 43 | raise errors.MaasError( 44 | "No records from endpoint {0} match the {1} query.".format( 45 | endpoint, query 46 | ) 47 | ) 48 | return records[0] if records else None 49 | 50 | def create_record(self, endpoint, payload, check_mode, timeout=120): 51 | if check_mode: 52 | return payload 53 | try: 54 | response = self.client.post( 55 | endpoint, payload, query=_query(), timeout=timeout 56 | ).json 57 | except TimeoutError as e: 58 | raise errors.MaasError(f"Request timed out: {e}") 59 | return response 60 | 61 | def update_record( 62 | self, endpoint, payload, check_mode, record=None, timeout=120 63 | ): 64 | # No action is possible when updating a record 65 | if check_mode: 66 | return payload 67 | try: 68 | response = self.client.patch( 69 | endpoint, payload, query=_query(), timeout=timeout 70 | ).json 71 | except TimeoutError as e: 72 | raise errors.MaasError(f"Request timed out: {e}") 73 | return response 74 | 75 | def delete_record(self, endpoint, check_mode, timeout=120): 76 | # No action is possible when deleting a record 77 | if check_mode: 78 | return 79 | try: 80 | response = self.client.delete(endpoint, timeout=timeout).json 81 | except TimeoutError as e: 82 | raise errors.MaasError(f"Request timed out: {e}") 83 | return response 84 | 85 | def put_record( 86 | self, 87 | endpoint, 88 | payload, 89 | check_mode, 90 | timeout=120, 91 | binary_data=None, 92 | headers=None, 93 | ): 94 | if check_mode: 95 | return payload 96 | 97 | try: 98 | response = self.client.put( 99 | endpoint, 100 | data=payload, 101 | query=_query(), 102 | timeout=timeout, 103 | binary_data=binary_data, 104 | headers=headers, 105 | ) 106 | except TimeoutError as e: 107 | raise errors.MaasError(f"Request timed out: {e}") 108 | return response 109 | -------------------------------------------------------------------------------- /tests/integration/targets/user/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | # ----------------------------------Cleanup--------------------------------------------------------------------------------- 11 | - name: Delete user John 12 | maas.maas.user: 13 | state: absent 14 | name: John 15 | # ----------------------------------Job------------------------------------------------------------------------------------- 16 | - name: Create user John 17 | maas.maas.user: 18 | state: present 19 | name: John 20 | password: john123 21 | email: john.smith@email.com 22 | is_admin: false 23 | register: new_user 24 | - ansible.builtin.assert: 25 | that: 26 | - new_user is succeeded 27 | - new_user is changed 28 | 29 | - name: Get info from user John 30 | maas.maas.user_info: 31 | name: John 32 | register: new_user_info 33 | - ansible.builtin.assert: 34 | that: 35 | - new_user_info is succeeded 36 | - new_user_info is not changed 37 | - new_user_info.record 38 | - new_user_info.record.username == "John" 39 | - new_user_info.record.email == "john.smith@email.com" 40 | - new_user_info.record.is_superuser == 0 41 | 42 | # ----------------------------------Idempotence check------------------------------------------------------------------------ 43 | - name: Create user John (Idempotence) 44 | maas.maas.user: 45 | state: present 46 | name: John 47 | password: john123 48 | email: john.smith@email.com 49 | is_admin: false 50 | register: new_user 51 | - ansible.builtin.assert: 52 | that: 53 | - new_user is succeeded 54 | - new_user is not changed 55 | 56 | - name: Get info from user John (Idempotence) 57 | maas.maas.user_info: 58 | name: John 59 | register: new_user_info 60 | - ansible.builtin.assert: 61 | that: 62 | - new_user_info is succeeded 63 | - new_user_info is not changed 64 | - new_user_info.record 65 | - new_user_info.record.username == "John" 66 | - new_user_info.record.email == "john.smith@email.com" 67 | - new_user_info.record.is_superuser == 0 68 | 69 | # ----------------------------------Job------------------------------------------------------------------------------------- 70 | - name: Delete user John 71 | maas.maas.user: 72 | state: absent 73 | name: John 74 | register: deleted_user 75 | - ansible.builtin.assert: 76 | that: 77 | - deleted_user is succeeded 78 | - deleted_user is changed 79 | 80 | - name: Get info from user John after delete 81 | maas.maas.user_info: 82 | name: John 83 | register: deleted_user_info 84 | - ansible.builtin.assert: 85 | that: 86 | - deleted_user_info is succeeded 87 | - deleted_user_info is not changed 88 | - deleted_user_info.record == None 89 | 90 | # ----------------------------------Idempotence check------------------------------------------------------------------------ 91 | - name: Delete user John (Idempotence) 92 | maas.maas.user: 93 | state: absent 94 | name: John 95 | register: deleted_user 96 | - ansible.builtin.assert: 97 | that: 98 | - deleted_user is succeeded 99 | - deleted_user is not changed 100 | 101 | - name: Get info from user John after delete (Idempotence) 102 | maas.maas.user_info: 103 | name: John 104 | register: deleted_user_info 105 | - ansible.builtin.assert: 106 | that: 107 | - deleted_user_info is succeeded 108 | - deleted_user_info is not changed 109 | - deleted_user_info.record == None 110 | -------------------------------------------------------------------------------- /plugins/modules/space_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: space_info 13 | 14 | author: 15 | - Polona Mihalič (@PolonaM) 16 | short_description: Returns info about network spaces. 17 | description: 18 | - Plugin returns information about all network spaces or specific network space if I(name) is provided. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: 24 | name: 25 | description: 26 | - Name of the network space to be listed. 27 | - Serves as unique identifier of the network space. 28 | type: str 29 | """ 30 | 31 | EXAMPLES = r""" 32 | - name: Get list of all network spaces 33 | maas.maas.space_info: 34 | cluster_instance: 35 | host: host-ip 36 | token_key: token-key 37 | token_secret: token-secret 38 | customer_key: customer-key 39 | 40 | - name: Get info about a specific network space 41 | maas.maas.space_info: 42 | cluster_instance: 43 | host: host-ip 44 | token_key: token-key 45 | token_secret: token-secret 46 | customer_key: customer-key 47 | name: my-network-space 48 | """ 49 | 50 | RETURN = r""" 51 | records: 52 | description: 53 | - Network space info list. 54 | returned: success 55 | type: list 56 | sample: 57 | id: -1 58 | name: undefined 59 | resource_uri: /MAAS/api/2.0/spaces/-1/ 60 | subnets: 61 | - name: 10.10.10.0/24 62 | description: "" 63 | vlan: 64 | vid: 0 65 | mtu: 1500 66 | dhcp_on: true 67 | external_dhcp: null 68 | relay_vlan: null 69 | fabric: fabric-1 70 | id: 5002 71 | space: undefined 72 | fabric_id: 1 73 | name: untagged 74 | primary_rack: kwxmgm 75 | secondary_rack: null 76 | resource_uri: /MAAS/api/2.0/vlans/5002/ 77 | cidr: 10.10.10.0/24 78 | rdns_mode: 2 79 | gateway_ip: 10.10.10.1 80 | dns_servers: [] 81 | allow_dns: true 82 | allow_proxy: true 83 | active_discovery: false 84 | managed: true 85 | disabled_boot_architectures: [] 86 | id: 2 87 | space: undefined 88 | resource_uri: /MAAS/api/2.0/subnets/2/ 89 | vlans: 90 | - vid: 0 91 | mtu: 1500 92 | dhcp_on: true 93 | external_dhcp: null 94 | relay_vlan: null 95 | fabric: fabric-1 96 | id: 5002 97 | space: undefined 98 | fabric_id: 1 99 | name: untagged 100 | primary_rack: kwxmgm 101 | secondary_rack: null 102 | resource_uri: /MAAS/api/2.0/vlans/5002/ 103 | """ 104 | 105 | 106 | from ansible.module_utils.basic import AnsibleModule 107 | 108 | from ..module_utils import arguments, errors 109 | from ..module_utils.client import Client 110 | from ..module_utils.cluster_instance import get_oauth1_client 111 | from ..module_utils.space import Space 112 | 113 | 114 | def run(module, client: Client): 115 | if module.params["name"]: 116 | space = Space.get_by_name(module, client, must_exist=True) 117 | response = [space.to_ansible()] 118 | else: 119 | response = client.get("/api/2.0/spaces/").json 120 | return response 121 | 122 | 123 | def main(): 124 | module = AnsibleModule( 125 | supports_check_mode=True, 126 | argument_spec=dict( 127 | arguments.get_spec("cluster_instance"), 128 | name=dict(type="str"), 129 | ), 130 | ) 131 | 132 | try: 133 | client = get_oauth1_client(module.params) 134 | records = run(module, client) 135 | module.exit_json(changed=False, records=records) 136 | except errors.MaasError as e: 137 | module.fail_json(msg=str(e)) 138 | 139 | 140 | if __name__ == "__main__": 141 | main() 142 | -------------------------------------------------------------------------------- /tests/integration/targets/vm_host_machine/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | vars: 8 | vm_host: "{% if test_existing_vm_host is defined %}{{ test_existing_vm_host }}{% else %}{{ test_lxd_host.hostname }}{% endif %}" 9 | 10 | 11 | block: 12 | # ----------------------------------Cleanup---------------------------------------------------------------------------- 13 | - name: Delete machine 14 | maas.maas.instance: 15 | fqdn: "integration-test.{{ test_domain }}" 16 | state: absent 17 | # ----------------------------------Job---------------------------------------------------------------------------- 18 | - name: Create virtual machine with name integration-test two disks and a network interface. 19 | maas.maas.vm_host_machine: 20 | vm_host: "{{ vm_host }}" 21 | hostname: integration-test 22 | cores: 2 23 | memory: 2048 24 | network_interfaces: 25 | label_name: my_first 26 | subnet_cidr: "{{ test_subnet }}" 27 | storage_disks: 28 | - size_gigabytes: 3 29 | - size_gigabytes: 5 30 | register: machine 31 | - ansible.builtin.assert: 32 | that: 33 | - machine is succeeded 34 | - machine is changed 35 | 36 | - name: Get info from created machine. 37 | maas.maas.machine_info: 38 | fqdn: "{{ machine.record.fqdn }}" 39 | register: machine_info 40 | - ansible.builtin.assert: 41 | that: 42 | - machine_info is succeeded 43 | - machine_info is not changed 44 | - machine_info.records 45 | - machine_info.records | length == 1 46 | - machine_info.records.0.hostname == "integration-test" 47 | - machine_info.records.0.blockdevice_set | length == 2 48 | - machine_info.records.0.cpu_count == 2 49 | - machine_info.records.0.interface_set | length == 1 50 | - machine_info.records.0.interface_set.0.name == "my_first" 51 | - machine_info.records.0.interface_set.0.links.0.subnet.cidr == "{{ test_subnet }}" 52 | - machine_info.records.0.memory == 2048 53 | 54 | # ----------------------------------Idempotence check------------------------------------------------------------------------ 55 | - name: Create virtual machine with name integration-test two disks and a network interface. (Idempotence) 56 | maas.maas.vm_host_machine: 57 | vm_host: "{{ vm_host }}" 58 | hostname: integration-test 59 | cores: 2 60 | memory: 2048 61 | network_interfaces: 62 | label_name: my_first 63 | subnet_cidr: "10.10.10.0/24" 64 | storage_disks: 65 | - size_gigabytes: 3 66 | - size_gigabytes: 5 67 | register: machine_idempotence 68 | - ansible.builtin.assert: 69 | that: 70 | - machine_idempotence is succeeded 71 | - machine_idempotence is not changed 72 | 73 | - name: Get info from created machine. (Idempotence) 74 | maas.maas.machine_info: 75 | fqdn: "{{ machine.record.fqdn }}" 76 | register: machine_info 77 | - ansible.builtin.assert: 78 | that: 79 | - machine_info is succeeded 80 | - machine_info is not changed 81 | - machine_info.records 82 | - machine_info.records | length == 1 83 | - machine_info.records.0.hostname == "integration-test" 84 | - machine_info.records.0.blockdevice_set | length == 2 85 | - machine_info.records.0.cpu_count == 2 86 | - machine_info.records.0.interface_set | length == 1 87 | - machine_info.records.0.interface_set.0.name == "my_first" 88 | - machine_info.records.0.interface_set.0.links.0.subnet.cidr == "{{ test_subnet }}" 89 | - machine_info.records.0.memory == 2048 90 | 91 | # ----------------------------------Cleanup---------------------------------------------------------------------------- 92 | - name: Delete machine 93 | maas.maas.instance: 94 | fqdn: "integration-test.{{ test_domain }}" 95 | state: absent 96 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_partition.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils import errors 15 | from ansible_collections.maas.maas.plugins.module_utils.client import Response 16 | from ansible_collections.maas.maas.plugins.module_utils.partition import ( 17 | Partition, 18 | ) 19 | 20 | pytestmark = pytest.mark.skipif( 21 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 22 | ) 23 | 24 | 25 | class TestMapper: 26 | def test_from_maas(self): 27 | partition_maas_dict = dict( 28 | id=5014, 29 | system_id="machine-id", 30 | device_id=12, 31 | bootable=True, 32 | tags=["my-tag", "my-tag2"], 33 | size=100000, 34 | filesystem=dict( 35 | fstype="ext4", 36 | label="media", 37 | mount_point="/media", 38 | mount_options="options", 39 | ), 40 | ) 41 | partition = Partition( 42 | partition_maas_dict["id"], 43 | partition_maas_dict["system_id"], 44 | partition_maas_dict["device_id"], 45 | partition_maas_dict["size"], 46 | partition_maas_dict["bootable"], 47 | partition_maas_dict["tags"], 48 | partition_maas_dict["filesystem"]["fstype"], 49 | partition_maas_dict["filesystem"]["label"], 50 | partition_maas_dict["filesystem"]["mount_point"], 51 | partition_maas_dict["filesystem"]["mount_options"], 52 | ) 53 | results = Partition.from_maas(partition_maas_dict) 54 | assert results == partition 55 | 56 | 57 | class TestGet: 58 | def test_get_by_id_200(self, client, mocker): 59 | id = 5 60 | machine_id = "machine-id" 61 | block_device_id = 10 62 | client.get.return_value = Response( 63 | 200, 64 | '{"id":5, "system_id":"machine-id","device_id":10, "bootable":true, "tags":[], "size":100000,\ 65 | "filesystem":{"fstype":"ext4", "label":"media", "mount_point": "/media", "mount_options":"options"}}', 66 | ) 67 | partition = Partition( 68 | id=5, 69 | machine_id="machine-id", 70 | block_device_id=10, 71 | size=100000, 72 | bootable=True, 73 | tags=[], 74 | fstype="ext4", 75 | label="media", 76 | mount_point="/media", 77 | mount_options="options", 78 | ) 79 | results = Partition.get_by_id( 80 | id, client, machine_id, block_device_id, must_exist=False 81 | ) 82 | 83 | client.get.assert_called_with( 84 | "/api/2.0/nodes/machine-id/blockdevices/10/partition/5", 85 | ) 86 | assert results == partition 87 | 88 | def test_get_by_id_404(self, client, mocker): 89 | id = 5 90 | machine_id = "machine-id" 91 | block_device_id = 10 92 | client.get.return_value = Response(404, "{}") 93 | results = Partition.get_by_id( 94 | id, client, machine_id, block_device_id, must_exist=False 95 | ) 96 | 97 | client.get.assert_called_with( 98 | "/api/2.0/nodes/machine-id/blockdevices/10/partition/5", 99 | ) 100 | assert results is None 101 | 102 | def test_get_by_id_404_must_exist(self, client, mocker): 103 | id = 5 104 | machine_id = "machine-id" 105 | block_device_id = 10 106 | client.get.return_value = Response(404, "{}") 107 | 108 | with pytest.raises(errors.PartitionNotFound) as exc: 109 | Partition.get_by_id( 110 | id, client, machine_id, block_device_id, must_exist=True 111 | ) 112 | 113 | assert "Partition - 5 - not found" in str(exc.value) 114 | -------------------------------------------------------------------------------- /tests/integration/targets/dns_domain/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | # Make first domain default, so we can delete test one if it staied default 11 | - name: List domains 12 | maas.maas.dns_domain_info: 13 | register: domains 14 | 15 | - ansible.builtin.set_fact: 16 | default_domain_name: "{{ domains.records[0].name }}" 17 | 18 | - name: Make first domain default 19 | maas.maas.dns_domain: &set_default 20 | name: "{{ default_domain_name }}" 21 | is_default: true 22 | state: present 23 | 24 | # Ensure that we start with clean environment 25 | - name: Delete domain - to have clean environment 26 | maas.maas.dns_domain: &delete 27 | name: test-domain 28 | state: absent 29 | 30 | # List all domains for later 31 | - name: List domains 32 | maas.maas.dns_domain_info: 33 | register: domains 34 | 35 | 36 | # Ensure that creating new domain reports changed 37 | - name: Create domain 38 | maas.maas.dns_domain: &create 39 | name: test-domain 40 | state: present 41 | register: test_domain 42 | 43 | - ansible.builtin.assert: 44 | that: 45 | - test_domain is changed 46 | - test_domain.record.name == "test-domain" 47 | - test_domain.record.is_default == false 48 | - test_domain.record.ttl is none 49 | - test_domain.record.authoritative == true 50 | 51 | # Ensure that creation is idempotent 52 | - name: Recreate domain 53 | maas.maas.dns_domain: *create 54 | register: test_domain 55 | 56 | - ansible.builtin.assert: 57 | that: 58 | - test_domain is not changed 59 | - test_domain.record.name == "test-domain" 60 | 61 | # Check setting one as default works 62 | - name: Set as default 63 | maas.maas.dns_domain: 64 | name: test-domain 65 | state: present 66 | is_default: true 67 | register: test_domain 68 | 69 | - ansible.builtin.assert: 70 | that: 71 | - test_domain is changed 72 | - test_domain.record.is_default == true 73 | 74 | # Check that setting optional value is working 75 | - name: Set ttl 76 | maas.maas.dns_domain: 77 | name: test-domain 78 | state: present 79 | ttl: 1800 80 | register: test_domain 81 | 82 | - ansible.builtin.assert: 83 | that: 84 | - test_domain is changed 85 | - test_domain.record.ttl == 1800 86 | - test_domain.record.authoritative == true 87 | 88 | # Check that omitting optional value is working. Authoritative changed, ttl skipped 89 | - name: Skip setting ttl 90 | maas.maas.dns_domain: 91 | name: test-domain 92 | state: present 93 | authoritative: false 94 | register: test_domain 95 | 96 | - ansible.builtin.assert: 97 | that: 98 | - test_domain is changed 99 | - test_domain.record.ttl == 1800 100 | - test_domain.record.authoritative == false 101 | 102 | # Deleting default should fail 103 | - name: Delete test domain, which is default 104 | maas.maas.dns_domain: *delete 105 | register: test_domain 106 | failed_when: test_domain is not failed 107 | 108 | # Ensure deleting reports changed 109 | - name: Make other domain default 110 | maas.maas.dns_domain: *set_default 111 | 112 | - name: Delete test domain 113 | maas.maas.dns_domain: *delete 114 | register: test_domain 115 | 116 | - ansible.builtin.assert: 117 | that: 118 | - test_domain is changed 119 | - test_domain.record is none 120 | 121 | # Ensure deleting of something that does not exist just returns 122 | - name: Redelete domain 123 | maas.maas.dns_domain: *delete 124 | register: test_domain 125 | 126 | - ansible.builtin.assert: 127 | that: 128 | - test_domain is not changed 129 | - test_domain.record is none 130 | -------------------------------------------------------------------------------- /plugins/modules/vm_host_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: vm_host_info 13 | 14 | author: 15 | - Jure Medvesek (@juremedvesek) 16 | short_description: Returns info about vm hosts. 17 | description: 18 | - Plugin returns information about all or specific vm hosts if I(name) is provided. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: 24 | name: 25 | description: 26 | - Name of the specific vm host to be listed. 27 | - Serves as unique identifier of the vm host. 28 | - If vm host is not found the task will FAIL. 29 | type: str 30 | """ 31 | 32 | EXAMPLES = r""" 33 | - name: Get list of all hosts. 34 | maas.maas.vm_host_info: 35 | cluster_instance: 36 | host: host-ip 37 | token_key: token-key 38 | token_secret: token-secret 39 | customer_key: customer-key 40 | 41 | - name: Get info about a specific vm host. 42 | maas.maas.vm_host_info: 43 | cluster_instance: 44 | host: host-ip 45 | token_key: token-key 46 | token_secret: token-secret 47 | customer_key: customer-key 48 | name: sunny-raptor 49 | """ 50 | 51 | RETURN = r""" 52 | records: 53 | description: 54 | - List records of vm hosts. 55 | returned: success 56 | type: list 57 | sample: 58 | - architectures: 59 | - amd64/generic 60 | available: 61 | cores: 1 62 | local_storage: 6884062720 63 | memory: 4144 64 | capabilities: 65 | - composable 66 | - dynamic_local_storage 67 | - over_commit 68 | - storage_pools 69 | cpu_over_commit_ratio: 1.0 70 | default_macvlan_mode: null 71 | host: 72 | __incomplete__: true 73 | system_id: d6car8 74 | id: 1 75 | memory_over_commit_ratio: 1.0 76 | name: sunny-raptor 77 | pool: 78 | description: Default pool 79 | id: 0 80 | name: default 81 | resource_uri: /MAAS/api/2.0/resourcepool/0/ 82 | resource_uri: /MAAS/api/2.0/vm-hosts/1/ 83 | storage_pools: 84 | - available: 6884062720 85 | default: true 86 | id: default 87 | name: default 88 | path: /var/snap/lxd/common/lxd/disks/default.img 89 | total: 22884062720 90 | type: zfs 91 | used: 16000000000 92 | tags: 93 | - pod-console-logging 94 | total: 95 | cores: 4 96 | local_storage: 22884062720 97 | memory: 8192 98 | type: lxd 99 | used: 100 | cores: 3 101 | local_storage: 16000000000 102 | memory: 4048 103 | version: '5.5' 104 | zone: 105 | description: '' 106 | id: 1 107 | name: default 108 | resource_uri: /MAAS/api/2.0/zones/default/ 109 | """ 110 | 111 | from ansible.module_utils.basic import AnsibleModule 112 | 113 | from ..module_utils import arguments, errors 114 | from ..module_utils.client import Client 115 | from ..module_utils.cluster_instance import get_oauth1_client 116 | from ..module_utils.vmhost import VMHost 117 | 118 | 119 | def run(module, client: Client): 120 | if module.params["name"]: 121 | vm_host = VMHost.get_by_name(module, client, must_exist=True) 122 | response = [vm_host.get(client)] 123 | else: 124 | response = client.get("/api/2.0/vm-hosts/").json 125 | return response 126 | 127 | 128 | def main(): 129 | module = AnsibleModule( 130 | supports_check_mode=True, 131 | argument_spec=dict( 132 | arguments.get_spec("cluster_instance"), 133 | name=dict(type="str"), 134 | ), 135 | ) 136 | 137 | try: 138 | client = get_oauth1_client(module.params) 139 | records = run(module, client) 140 | module.exit_json(changed=False, records=records) 141 | except errors.MaasError as e: 142 | module.fail_json(msg=str(e)) 143 | 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /plugins/modules/vlan_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: vlan_info 13 | 14 | author: 15 | - Polona Mihalič (@PolonaM) 16 | short_description: Returns info about VLANs. 17 | description: 18 | - Plugin returns information about all VLANs on a specific fabric or specific VLAN on a specific fabric. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: 24 | fabric_name: 25 | description: 26 | - Name of the fabric whose VLANs will be listed. 27 | - Serves as unique identifier of the fabric. 28 | - If fabric is not found, the task will FAIL. 29 | type: str 30 | required: true 31 | vid: 32 | description: 33 | - Traffic segregation ID of the VLAN to be listed. 34 | - Serves as unique identifier of the VLAN. 35 | - If VLAN is not found, the task will FAIL. 36 | type: str 37 | vlan_name: 38 | description: 39 | - Name of the VLAN to be listed. 40 | - Serves as unique identifier of the VLAN if I(vid) is not provided. 41 | - If VLAN is not found, the task will FAIL. 42 | type: str 43 | """ 44 | 45 | EXAMPLES = r""" 46 | - name: Get list of all VLANs on a specific fabric 47 | maas.maas.vlan_info: 48 | cluster_instance: 49 | host: host-ip 50 | token_key: token-key 51 | token_secret: token-secret 52 | customer_key: customer-key 53 | fabric_name: fabric-7 54 | 55 | - name: Get info about a specific VLAN on a specific fabric - by vlan name 56 | maas.maas.vlan_info: 57 | cluster_instance: 58 | host: host-ip 59 | token_key: token-key 60 | token_secret: token-secret 61 | customer_key: customer-key 62 | fabric_name: fabric-7 63 | vlan_name: vlan-5 64 | 65 | - name: Get info about a specific VLAN on a specific fabric - by vid 66 | maas.maas.vlan_info: 67 | cluster_instance: 68 | host: host-ip 69 | token_key: token-key 70 | token_secret: token-secret 71 | customer_key: customer-key 72 | fabric_name: fabric-7 73 | vid: 5 74 | """ 75 | 76 | RETURN = r""" 77 | records: 78 | description: 79 | - VLANs on a specific fabric info list. 80 | returned: success 81 | type: list 82 | sample: 83 | - dhcp_on: false 84 | external_dhcp: null 85 | fabric: fabric-7 86 | fabric_id: 7 87 | id: 5014 88 | mtu: 1500 89 | name: vlan-5 90 | primary_rack: null 91 | relay_vlan: null 92 | resource_uri: /MAAS/api/2.0/vlans/5014/ 93 | secondary_rack: null 94 | space: undefined 95 | vid: 5 96 | """ 97 | 98 | 99 | from ansible.module_utils.basic import AnsibleModule 100 | 101 | from ..module_utils import arguments, errors 102 | from ..module_utils.client import Client 103 | from ..module_utils.cluster_instance import get_oauth1_client 104 | from ..module_utils.fabric import Fabric 105 | from ..module_utils.vlan import Vlan 106 | 107 | 108 | def run(module, client: Client): 109 | fabric = Fabric.get_by_name( 110 | module, client, must_exist=True, name_field_ansible="fabric_name" 111 | ) 112 | if module.params["vid"]: 113 | vlan = Vlan.get_by_vid(module, client, fabric.id, must_exist=True) 114 | response = [vlan.to_ansible()] 115 | elif module.params["vlan_name"]: 116 | vlan = Vlan.get_by_name(module, client, fabric.id, must_exist=True) 117 | response = [vlan.to_ansible()] 118 | else: 119 | response = client.get(f"/api/2.0/fabrics/{fabric.id}/vlans/").json 120 | return response 121 | 122 | 123 | def main(): 124 | module = AnsibleModule( 125 | supports_check_mode=True, 126 | argument_spec=dict( 127 | arguments.get_spec("cluster_instance"), 128 | fabric_name=dict(type="str", required=True), 129 | vlan_name=dict(type="str"), 130 | vid=dict(type="str"), 131 | ), 132 | ) 133 | 134 | try: 135 | client = get_oauth1_client(module.params) 136 | records = run(module, client) 137 | module.exit_json(changed=False, records=records) 138 | except errors.MaasError as e: 139 | module.fail_json(msg=str(e)) 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /tests/integration/targets/aaa_vm_host_pre_tasks/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | block: 9 | # ----------------------------------Cleanup------------------------------------ 10 | - name: VM host cleanup 11 | maas.maas.vm_host: 12 | vm_host_name: "{{ item }}" 13 | state: absent 14 | loop: 15 | - "{{ test_lxd_host.hostname }}" 16 | - "{{ test_virsh_host.hostname }}" 17 | 18 | # ------------------------------------Job------------------------------------ 19 | - name: Add machine for LXD host. 20 | maas.maas.machine: 21 | state: present 22 | power_type: "{{ test_lxd_host.power_type }}" 23 | power_parameters: 24 | power_address: "{{ test_lxd_host.power_address }}" 25 | power_user: "{{ test_lxd_host.power_user }}" 26 | power_pass: "{{ test_lxd_host.power_pass }}" 27 | power_driver: "{{ test_lxd_host.power_driver }}" 28 | power_boot_type: "{{ test_lxd_host.power_boot_type }}" 29 | pxe_mac_address: "{{ test_lxd_host.mac_address }}" 30 | hostname: "{{ test_lxd_host.hostname }}" 31 | architecture: "{{ test_lxd_host.architecture }}" 32 | domain: "{{ test_domain }}" 33 | pool: default 34 | zone: default 35 | when: test_lxd_host is defined 36 | 37 | - name: Add machine for VIRSH host. 38 | maas.maas.machine: 39 | state: present 40 | power_type: "{{ test_virsh_host.power_type }}" 41 | power_parameters: 42 | power_address: "{{ test_virsh_host.power_address }}" 43 | power_user: "{{ test_virsh_host.power_user }}" 44 | power_pass: "{{ test_virsh_host.power_pass }}" 45 | power_driver: "{{ test_virsh_host.power_driver }}" 46 | power_boot_type: "{{ test_virsh_host.power_boot_type }}" 47 | pxe_mac_address: "{{ test_virsh_host.mac_address }}" 48 | hostname: "{{ test_virsh_host.hostname }}" 49 | architecture: "{{ test_virsh_host.architecture }}" 50 | domain: "{{ test_domain }}" 51 | pool: default 52 | zone: default 53 | when: test_virsh_host is defined 54 | 55 | - name: Register LXD VM host 56 | maas.maas.vm_host: 57 | state: present 58 | vm_host_name: "{{ test_lxd_host.hostname }}" 59 | machine_fqdn: "{{ test_lxd_host.hostname }}.{{ test_domain }}" 60 | power_parameters: 61 | power_type: lxd 62 | timeout: 3600 63 | cpu_over_commit_ratio: 3 64 | memory_over_commit_ratio: 4 65 | default_macvlan_mode: bridge 66 | zone: default 67 | pool: default 68 | tags: my-tag 69 | register: vm_host 70 | when: test_lxd_host is defined 71 | - ansible.builtin.assert: 72 | that: 73 | - vm_host is changed 74 | - vm_host.record.name == "{{ test_lxd_host.hostname }}" 75 | - vm_host.record.type == "lxd" 76 | - vm_host.record.cpu_over_commit_ratio == 3.0 77 | - vm_host.record.memory_over_commit_ratio == 4.0 78 | - vm_host.record.default_macvlan_mode == "bridge" 79 | - vm_host.record.zone.name == "default" 80 | - vm_host.record.pool.name == "default" 81 | # - vm_host.record.tags == ["my-tag", "pod-console-logging"] this will work after tag bug is resolved 82 | when: test_lxd_host is defined 83 | 84 | - name: Register VIRSH host 85 | maas.maas.vm_host: 86 | state: present 87 | vm_host_name: "{{ test_virsh_host.hostname }}" 88 | machine_fqdn: "{{ test_virsh_host.hostname }}.{{ test_domain }}" 89 | power_parameters: 90 | power_type: virsh 91 | timeout: 3600 92 | cpu_over_commit_ratio: 1 93 | memory_over_commit_ratio: 2 94 | default_macvlan_mode: bridge 95 | zone: default 96 | pool: default 97 | tags: my-tag 98 | register: vm_host 99 | when: test_virsh_host is defined 100 | - ansible.builtin.assert: 101 | that: 102 | - vm_host is changed 103 | - vm_host.record.name == "{{ test_virsh_host.hostname }}" 104 | - vm_host.record.type == "virsh" 105 | - vm_host.record.cpu_over_commit_ratio == 1.0 106 | - vm_host.record.memory_over_commit_ratio == 2.0 107 | - vm_host.record.default_macvlan_mode == "bridge" 108 | - vm_host.record.zone.name == "default" 109 | - vm_host.record.pool.name == "default" 110 | # - vm_host.record.tags == ["my-tag", "virtual", "pod-console-logging"] this will work after tag bug is resolved 111 | when: test_virsh_host is defined 112 | -------------------------------------------------------------------------------- /tests/integration/targets/machine/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | - name: Delete machine-test 11 | maas.maas.machine: 12 | fqdn: "machine-test.{{ test_domain }}" 13 | state: absent 14 | 15 | - name: Delete updated-machine 16 | maas.maas.machine: 17 | fqdn: "updated-machine.{{ test_domain }}" 18 | state: absent 19 | 20 | - name: Add machine - missing parameter 21 | maas.maas.machine: 22 | state: present 23 | power_parameters: 24 | power_address: 172.16.117.70:8443 25 | instance_name: machine-test-instance-name 26 | pxe_mac_address: 00:00:00:00:00:01 27 | hostname: machine-test 28 | architecture: amd64/generic 29 | domain: "{{ test_domain }}" 30 | pool: default 31 | zone: default 32 | min_hwe_kernel: hwe-20.04 33 | ignore_errors: true 34 | register: machine 35 | - ansible.builtin.assert: 36 | that: 37 | - machine is failed 38 | - "'Missing value - power_type, power_parameters or pxe_mac_address' in machine.msg" 39 | 40 | - name: Add machine. 41 | maas.maas.machine: 42 | state: present 43 | power_type: lxd 44 | power_parameters: 45 | power_address: 172.16.117.70:8443 46 | instance_name: machine-test-instance-name 47 | pxe_mac_address: 00:00:00:00:00:01 48 | hostname: machine-test 49 | architecture: amd64/generic 50 | domain: "{{ test_domain }}" 51 | pool: default 52 | zone: default 53 | min_hwe_kernel: hwe-20.04 54 | failed_when: false # Here we get error: "Unexpected response - 503 b'No rack controllers can access the BMC of node machine-test'" 55 | # register: machine 56 | # - ansible.builtin.assert: 57 | # that: 58 | # - machine is changed 59 | # - machine.record.hostname == "machine-test" 60 | # - machine.record.power_type == "lxd" 61 | # - machine.record.network_interfaces.0.mac_address == "00:00:00:00:00:01" 62 | # - machine.record.architecture == "amd64/generic" 63 | # - machine.record.min_hwe_kernel == "hwe-20.04" 64 | 65 | - name: Update existing machine 66 | maas.maas.machine: 67 | state: present 68 | fqdn: "machine-test.{{ test_domain }}" 69 | power_type: virsh 70 | power_parameters: 71 | power_address: 172.16.117.70:8443 72 | power_pass: pass 73 | power_id: virsh_vm_id 74 | # architecture: i386/generic 75 | # "Unexpected response - 400 b'{\"architecture\": [\"\\'i386/generic\\' is not a valid architecture. It should be one of: \\'amd64/generic\\'.\"]}'" 76 | hostname: updated-machine 77 | # domain: new-domain # domain, pool and zone needs to be added first to test this 78 | # pool: new-pool 79 | # zone: new-zone 80 | min_hwe_kernel: ga-20.04 81 | register: machine 82 | - ansible.builtin.assert: 83 | that: 84 | - machine is changed 85 | - machine.record.hostname == "updated-machine" 86 | - machine.record.power_type == "virsh" 87 | - machine.record.architecture == "amd64/generic" 88 | - machine.record.min_hwe_kernel == "ga-20.04" 89 | 90 | - name: Update existing machine - idempotence 91 | maas.maas.machine: 92 | state: present 93 | fqdn: "updated-machine.{{ test_domain }}" 94 | # updating power_type and power_parameters will never be idempoten because we can't read all the values 95 | architecture: amd64/generic 96 | hostname: updated-machine 97 | # domain: new-domain 98 | # pool: new-pool 99 | # zone: new-zone 100 | min_hwe_kernel: ga-20.04 101 | register: machine 102 | - ansible.builtin.assert: 103 | that: 104 | - machine is not changed 105 | - machine.record.hostname == "updated-machine" 106 | - machine.record.power_type == "virsh" 107 | - machine.record.architecture == "amd64/generic" 108 | - machine.record.min_hwe_kernel == "ga-20.04" 109 | 110 | - name: Delete machine 111 | maas.maas.instance: 112 | fqdn: "updated-machine.{{ test_domain }}" 113 | state: absent 114 | register: machine 115 | - ansible.builtin.assert: 116 | that: 117 | - machine is changed 118 | 119 | - name: Delete machine - idempotence 120 | maas.maas.instance: 121 | fqdn: "updated-machine.{{ test_domain }}" 122 | state: absent 123 | register: machine 124 | - ansible.builtin.assert: 125 | that: 126 | - machine is not changed 127 | -------------------------------------------------------------------------------- /plugins/modules/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: user 13 | 14 | author: 15 | - Domen Dobnikar (@domen_dobnikar) 16 | short_description: Manage the user accounts. 17 | description: Create or delete user accounts. 18 | version_added: 1.0.0 19 | extends_documentation_fragment: 20 | - maas.maas.cluster_instance 21 | seealso: [] 22 | options: 23 | name: 24 | description: 25 | - The user name. 26 | - Identifier-style username for the user. 27 | type: str 28 | required: True 29 | password: 30 | description: The user password. 31 | type: str 32 | email: 33 | description: The user e-mail address. 34 | type: str 35 | is_admin: 36 | description: Indicating if the user is a MAAS administrator. 37 | type: bool 38 | default: False 39 | state: 40 | description: Preferred state of the user. 41 | choices: [ present, absent ] 42 | type: str 43 | required: True 44 | """ 45 | 46 | EXAMPLES = r""" 47 | - name: Create user John 48 | maas.maas.user: 49 | cluster_instance: 50 | host: host-ip 51 | token_key: token-key 52 | token_secret: token-secret 53 | customer_key: customer-key 54 | state: present 55 | name: John 56 | password: john123 57 | email: john.smith@email.com 58 | is_admin: false 59 | register: new_user 60 | - ansible.builtin.debug: 61 | var: new_user 62 | 63 | - name: Delete user John 64 | maas.maas.user: 65 | cluster_instance: 66 | host: host-ip 67 | token_key: token-key 68 | token_secret: token-secret 69 | customer_key: customer-key 70 | state: absent 71 | name: John 72 | is_admin: false 73 | register: deleted_user 74 | - ansible.builtin.debug: 75 | var: deleted_user 76 | """ 77 | 78 | RETURN = r""" 79 | record: 80 | description: 81 | - Created or deleted user account. 82 | returned: success 83 | type: dict 84 | sample: 85 | email: john@email.com 86 | is_admin: false 87 | is_local: true 88 | name: john 89 | """ 90 | 91 | from ansible.module_utils.basic import AnsibleModule 92 | 93 | from ..module_utils import arguments, errors 94 | from ..module_utils.cluster_instance import get_oauth1_client 95 | from ..module_utils.state import UserState 96 | from ..module_utils.user import User 97 | from ..module_utils.utils import is_changed 98 | 99 | 100 | def ensure_present(module, client): 101 | before = None 102 | after = None 103 | new_user = User.from_ansible(module.params) 104 | existing_user = User.get_by_name(module, client) 105 | if not existing_user: # Create user 106 | new_user.send_create_request(client, new_user.payload_for_create()) 107 | after = User.get_by_name(module, client).to_ansible() 108 | else: 109 | module.warn(f"User - {existing_user.name} - already exists.") 110 | return is_changed(before, after), after, dict(before=before, after=after) 111 | 112 | 113 | def ensure_absent(module, client): 114 | before = None 115 | after = None 116 | existing_user = User.get_by_name(module, client) 117 | if existing_user: 118 | before = existing_user.to_ansible() 119 | existing_user.send_delete_request(client) 120 | return is_changed(before, after), after, dict(before=before, after=after) 121 | 122 | 123 | def run(module, client): 124 | if module.params["state"] == UserState.present: 125 | changed, record, diff = ensure_present(module, client) 126 | else: 127 | changed, record, diff = ensure_absent(module, client) 128 | return changed, record, diff 129 | 130 | 131 | def main(): 132 | module = AnsibleModule( 133 | supports_check_mode=False, 134 | argument_spec=dict( 135 | arguments.get_spec("cluster_instance"), 136 | name=dict( 137 | type="str", 138 | required=True, 139 | ), 140 | password=dict( 141 | type="str", 142 | no_log=True, 143 | ), 144 | state=dict( 145 | type="str", 146 | choices=["present", "absent"], 147 | required=True, 148 | ), 149 | email=dict( 150 | type="str", 151 | ), 152 | is_admin=dict( 153 | type="bool", 154 | default=False, 155 | ), 156 | ), 157 | required_if=[("state", "present", ("password", "email"))], 158 | ) 159 | 160 | try: 161 | client = get_oauth1_client(module.params) 162 | changed, record, diff = run(module, client) 163 | module.exit_json(changed=changed, record=record, diff=diff) 164 | except errors.MaasError as e: 165 | module.fail_json(msg=str(e)) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | -------------------------------------------------------------------------------- /plugins/modules/network_interface_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: network_interface_info 13 | 14 | author: 15 | - Domen Dobnikar (@domen_dobnikar) 16 | short_description: Gets information about network interfaces on a specific device. 17 | description: 18 | - Get information from a specific device, can also filter based on mac_address. 19 | - Returns information about physical and linked network interfaces. 20 | version_added: 1.0.0 21 | extends_documentation_fragment: 22 | - maas.maas.cluster_instance 23 | seealso: [] 24 | options: 25 | machine: 26 | description: 27 | - Fully qualified domain name of the machine to be deleted, deployed or released. 28 | - Serves as unique identifier of the machine. 29 | - If machine is not found the task will FAIL. 30 | type: str 31 | required: True 32 | mac_address: 33 | description: Mac address of the network interface. 34 | type: str 35 | """ 36 | 37 | EXAMPLES = r""" 38 | - name: List nics from instance machine 39 | maas.maas.network_interface_physical_info: 40 | cluster_instance: 41 | host: host-ip 42 | token_key: token-key 43 | token_secret: token-secret 44 | customer_key: customer-key 45 | machine: instance.maas 46 | mac_address: 00:16:3e:46:25:e3 47 | register: nic_info 48 | - ansible.builtin.debug: 49 | var: nic_info 50 | """ 51 | 52 | RETURN = r""" 53 | record: 54 | description: 55 | - Get information from nic. 56 | returned: success 57 | type: dict 58 | sample: 59 | children: [] 60 | discovered: [] 61 | effective_mtu: 1500 62 | enabled: true 63 | firmware_version: null 64 | id: 208 65 | interface_speed: 0 66 | link_connected: true 67 | link_speed: 0 68 | links: 69 | - id: 1152 70 | mode: auto 71 | subnet: 72 | active_discovery: false 73 | allow_dns: true 74 | allow_proxy: true 75 | cidr: 10.10.10.0/24 76 | description: '' 77 | disabled_boot_architectures: [] 78 | dns_servers: [] 79 | gateway_ip: 10.10.10.1 80 | id: 2 81 | managed: true 82 | name: 10.10.10.0/24 83 | rdns_mode: 2 84 | resource_uri: /MAAS/api/2.0/subnets/2/ 85 | space: undefined 86 | vlan: 87 | dhcp_on: true 88 | external_dhcp: null 89 | fabric: fabric-1 90 | fabric_id: 1 91 | id: 5002 92 | mtu: 1500 93 | name: untagged 94 | primary_rack: kwxmgm 95 | relay_vlan: null 96 | resource_uri: /MAAS/api/2.0/vlans/5002/ 97 | secondary_rack: null 98 | space: undefined 99 | vid: 0 100 | mac_address: 00:16:3e:46:25:e3 101 | name: my_first 102 | numa_node: 0 103 | params: '' 104 | parents: [] 105 | product: null 106 | resource_uri: /MAAS/api/2.0/nodes/ks7wsq/interfaces/208/ 107 | sriov_max_vf: 0 108 | system_id: ks7wsq 109 | tags: [] 110 | type: physical 111 | vendor: null 112 | vlan: 113 | dhcp_on: true 114 | external_dhcp: null 115 | fabric: fabric-1 116 | fabric_id: 1 117 | id: 5002 118 | mtu: 1500 119 | name: untagged 120 | primary_rack: kwxmgm 121 | relay_vlan: null 122 | resource_uri: /MAAS/api/2.0/vlans/5002/ 123 | secondary_rack: null 124 | space: undefined 125 | vid: 0 126 | """ 127 | 128 | from ansible.module_utils.basic import AnsibleModule 129 | 130 | from ..module_utils import arguments, errors 131 | from ..module_utils.cluster_instance import get_oauth1_client 132 | from ..module_utils.machine import Machine 133 | 134 | 135 | def run(module, client): 136 | machine_obj = Machine.get_by_fqdn( 137 | module, client, must_exist=True, name_field_ansible="machine" 138 | ) 139 | if module.params["mac_address"]: 140 | nic_obj = machine_obj.find_nic_by_mac(module.params["mac_address"]) 141 | if nic_obj: 142 | response = client.get( 143 | f"/api/2.0/nodes/{machine_obj.id}/interfaces/{nic_obj.id}/", 144 | ).json 145 | else: 146 | response = client.get( 147 | f"/api/2.0/nodes/{machine_obj.id}/interfaces/", 148 | ).json 149 | return response 150 | 151 | 152 | def main(): 153 | module = AnsibleModule( 154 | supports_check_mode=True, 155 | argument_spec=dict( 156 | arguments.get_spec("cluster_instance"), 157 | machine=dict( 158 | type="str", 159 | required=True, 160 | ), 161 | mac_address=dict( 162 | type="str", 163 | ), 164 | ), 165 | ) 166 | 167 | try: 168 | client = get_oauth1_client(module.params) 169 | records = run(module, client) 170 | module.exit_json(changed=False, records=records) 171 | except errors.MaasError as e: 172 | module.fail_json(msg=str(e)) 173 | 174 | 175 | if __name__ == "__main__": 176 | main() 177 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils import errors 15 | from ansible_collections.maas.maas.plugins.module_utils.client import Response 16 | from ansible_collections.maas.maas.plugins.module_utils.tag import Tag 17 | 18 | pytestmark = pytest.mark.skipif( 19 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 20 | ) 21 | 22 | 23 | class TestRequest: 24 | def test_send_tag_request(self, client): 25 | machine_id = "test-id" 26 | tag_name = "test" 27 | client.post.return_value = Response(200, "[]") 28 | results = Tag.send_tag_request(client, machine_id, tag_name) 29 | assert results is None 30 | 31 | def test_send_untag_request(self, client): 32 | machine_id = "test-id" 33 | tag_name = "test" 34 | client.post.return_value = Response(200, "[]") 35 | results = Tag.send_untag_request(client, machine_id, tag_name) 36 | assert results is None 37 | 38 | def test_send_create_request(self, create_module, client): 39 | module = create_module( 40 | params=dict( 41 | instance=dict( 42 | host="https://0.0.0.0", 43 | client_key="client key", 44 | token_key="token key", 45 | token_secret="token secret", 46 | ), 47 | state="set", 48 | name="this_tag", 49 | machines=["this_machine", "that_machine"], 50 | ) 51 | ) 52 | client.post.return_value = Response(200, "[]") 53 | results = Tag.send_create_request(client, module) 54 | assert results is None 55 | 56 | 57 | class TestGet: 58 | def test_get_tag_by_name_must_exist_true(self, create_module, client): 59 | module = create_module( 60 | params=dict( 61 | instance=dict( 62 | host="https://0.0.0.0", 63 | client_key="client key", 64 | token_key="token key", 65 | token_secret="token secret", 66 | ), 67 | state="set", 68 | name="one", 69 | machines=["this_machine", "that_machine"], 70 | ) 71 | ) 72 | client.get.return_value = Response( 73 | 200, '[{"name":"one"}, {"name":"two"}]' 74 | ) 75 | results = Tag.get_tag_by_name(client, module, must_exist=True) 76 | assert results == {"name": "one"} 77 | 78 | def test_get_tag_by_name_must_exist_true_and_not_exist( 79 | self, create_module, client 80 | ): 81 | module = create_module( 82 | params=dict( 83 | instance=dict( 84 | host="https://0.0.0.0", 85 | client_key="client key", 86 | token_key="token key", 87 | token_secret="token secret", 88 | ), 89 | state="set", 90 | name="one", 91 | machines=["this_machine", "that_machine"], 92 | ) 93 | ) 94 | client.get.return_value = Response(200, "[]") 95 | with pytest.raises( 96 | errors.MaasError, 97 | match=f"Tag - {module.params['name']} - does not exist.", 98 | ): 99 | Tag.get_tag_by_name(client, module, must_exist=True) 100 | 101 | def test_get_tag_by_name_must_exist_false(self, create_module, client): 102 | module = create_module( 103 | params=dict( 104 | instance=dict( 105 | host="https://0.0.0.0", 106 | client_key="client key", 107 | token_key="token key", 108 | token_secret="token secret", 109 | ), 110 | state="set", 111 | name="one", 112 | machines=["this_machine", "that_machine"], 113 | ) 114 | ) 115 | client.get.return_value = Response( 116 | 200, '[{"name":"one"}, {"name":"two"}]' 117 | ) 118 | results = Tag.get_tag_by_name(client, module, must_exist=False) 119 | assert results == {"name": "one"} 120 | 121 | def test_get_tag_by_name_must_exist_false_and_not_exist( 122 | self, create_module, client 123 | ): 124 | module = create_module( 125 | params=dict( 126 | instance=dict( 127 | host="https://0.0.0.0", 128 | client_key="client key", 129 | token_key="token key", 130 | token_secret="token secret", 131 | ), 132 | state="set", 133 | name="one", 134 | machines=["this_machine", "that_machine"], 135 | ) 136 | ) 137 | client.get.return_value = Response(200, "[]") 138 | results = Tag.get_tag_by_name(client, module, must_exist=False) 139 | assert results is None 140 | -------------------------------------------------------------------------------- /tests/unit/plugins/modules/test_vlan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import sys 11 | 12 | import pytest 13 | 14 | from ansible_collections.maas.maas.plugins.module_utils.vlan import Vlan 15 | from ansible_collections.maas.maas.plugins.modules import vlan 16 | 17 | pytestmark = pytest.mark.skipif( 18 | sys.version_info < (2, 7), reason="requires python2.7 or higher" 19 | ) 20 | 21 | 22 | class TestMain: 23 | def test_all_params(self, run_main): 24 | params = dict( 25 | cluster_instance=dict( 26 | host="https://0.0.0.0", 27 | token_key="URCfn6EhdZ", 28 | token_secret="PhXz3ncACvkcK", 29 | customer_key="nzW4EBWjyDe", 30 | ), 31 | state="present", 32 | fabric_name="fabric-10", 33 | vid=5, 34 | vlan_name="vlan_name", 35 | new_vlan_name="new_vlan_name", 36 | description="vlan description", 37 | mtu=1500, 38 | dhcp_on=True, 39 | relay_vlan=17, 40 | space="my-space", 41 | ) 42 | 43 | success, result = run_main(vlan, params) 44 | 45 | assert success is True 46 | 47 | def test_minimal_set_of_params(self, run_main): 48 | params = dict( 49 | cluster_instance=dict( 50 | host="https://0.0.0.0", 51 | token_key="URCfn6EhdZ", 52 | token_secret="PhXz3ncACvkcK", 53 | customer_key="nzW4EBWjyDe", 54 | ), 55 | state="present", 56 | fabric_name="fabric-10", 57 | vid=5, 58 | ) 59 | 60 | success, result = run_main(vlan, params) 61 | 62 | assert success is True 63 | 64 | def test_fail(self, run_main): 65 | success, result = run_main(vlan) 66 | 67 | assert success is False 68 | assert ( 69 | "missing required arguments: fabric_name, state" in result["msg"] 70 | ) 71 | 72 | def test_required_one_of(self, run_main): 73 | params = dict( 74 | cluster_instance=dict( 75 | host="https://0.0.0.0", 76 | token_key="URCfn6EhdZ", 77 | token_secret="PhXz3ncACvkcK", 78 | customer_key="nzW4EBWjyDe", 79 | ), 80 | state="present", 81 | fabric_name="fabric-10", 82 | ) 83 | success, result = run_main(vlan, params) 84 | 85 | assert success is False 86 | assert ( 87 | "one of the following is required: vid, vlan_name" in result["msg"] 88 | ) 89 | 90 | 91 | class TestDataForCreateSpace: 92 | def test_data_for_create_vlan(self, create_module): 93 | module = create_module( 94 | params=dict( 95 | cluster_instance=dict( 96 | host="https://0.0.0.0", 97 | token_key="URCfn6EhdZ", 98 | token_secret="PhXz3ncACvkcK", 99 | customer_key="nzW4EBWjyDe", 100 | ), 101 | state="present", 102 | fabric_name="fabric-10", 103 | vid=5, 104 | vlan_name="vlan_name", 105 | new_vlan_name="new_vlan_name", 106 | description="vlan description", 107 | mtu=1500, 108 | dhcp_on=True, 109 | space="my-space", 110 | relay_vlan=17, 111 | ) 112 | ) 113 | data = vlan.data_for_create_vlan(module) 114 | 115 | assert data == dict( 116 | vid=5, 117 | name="vlan_name", 118 | description="vlan description", 119 | mtu=1500, 120 | space="my-space", 121 | relay_vlan=17, 122 | ) 123 | 124 | 125 | class TestDataForUpdateSpace: 126 | def test_data_for_update_vlan(self, create_module): 127 | module = create_module( 128 | params=dict( 129 | cluster_instance=dict( 130 | host="https://0.0.0.0", 131 | token_key="URCfn6EhdZ", 132 | token_secret="PhXz3ncACvkcK", 133 | customer_key="nzW4EBWjyDe", 134 | ), 135 | state="present", 136 | fabric_name="fabric-10", 137 | vid=5, 138 | new_vlan_name="new-name", 139 | description="vlan description", 140 | mtu=2000, 141 | dhcp_on=True, 142 | space="new-space", 143 | relay_vlan=17, 144 | ) 145 | ) 146 | old_vlan = Vlan( 147 | vid=5, 148 | name="old-name", 149 | mtu=1500, 150 | dhcp_on=False, 151 | space="old-space", 152 | ) 153 | data = vlan.data_for_update_vlan(module, old_vlan) 154 | 155 | assert data == dict( 156 | name="new-name", 157 | description="vlan description", # description is not returned 158 | mtu=2000, 159 | space="new-space", 160 | dhcp_on=True, 161 | relay_vlan=17, 162 | ) 163 | -------------------------------------------------------------------------------- /tests/unit/plugins/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | import json 11 | 12 | from ansible.module_utils import basic 13 | from ansible.module_utils._text import to_bytes 14 | import pytest 15 | 16 | from ansible_collections.maas.maas.plugins.module_utils.client import Client 17 | from ansible_collections.maas.maas.plugins.module_utils.rest_client import ( 18 | RestClient, 19 | ) 20 | 21 | 22 | @pytest.fixture 23 | def client(mocker): 24 | return mocker.Mock(spec=Client) 25 | 26 | 27 | @pytest.fixture 28 | def rest_client(mocker): 29 | return mocker.Mock(spec=RestClient(client=client)) 30 | 31 | 32 | @pytest.fixture 33 | def create_module(mocker): 34 | # Fixture for creating AnsibleModule instance mocks. All instance mocks are limited 35 | # in what method calls they allow in order to enforce rules for writing ServiceNow 36 | # modules. 37 | 38 | def constructor(params=None, check_mode=False): 39 | return mocker.Mock( 40 | spec_set=["check_mode", "deprecate", "params", "warn", "sha256"], 41 | params=params or {}, 42 | check_mode=check_mode, 43 | ) 44 | 45 | return constructor 46 | 47 | 48 | # Helpers for testing module invocation (parameter parsing and validation). Adapted from 49 | # https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html and made 50 | # into a reusable pytest fixture. 51 | 52 | 53 | class AnsibleRunEnd(Exception): 54 | def __init__(self, success, result): 55 | super(AnsibleRunEnd, self).__init__("End task run") 56 | 57 | self.success = success 58 | self.result = result 59 | 60 | 61 | def exit_json_mock(self, **result): 62 | raise AnsibleRunEnd(True, result) 63 | 64 | 65 | def fail_json_mock(self, **result): 66 | raise AnsibleRunEnd(False, result) 67 | 68 | 69 | def run_mock(module, client, another_client=None): 70 | return False, {}, dict(before={}, after={}) 71 | 72 | 73 | def run_mock_with_reboot(module, client, another_client=None): 74 | return False, {}, dict(before={}, after={}), False 75 | 76 | 77 | def run_mock_info(module, client, another_client=None): 78 | return False, [] 79 | 80 | 81 | @pytest.fixture 82 | def run_main(mocker): 83 | def runner(module, params=None): 84 | args = dict( 85 | ANSIBLE_MODULE_ARGS=dict( 86 | _ansible_remote_tmp="/tmp", 87 | _ansible_keep_remote_files=False, 88 | ), 89 | ) 90 | args["ANSIBLE_MODULE_ARGS"].update(params or {}) 91 | mocker.patch.object(basic, "_ANSIBLE_ARGS", to_bytes(json.dumps(args))) 92 | 93 | # We can mock the run function because we enforce module structure in our 94 | # development guidelines. 95 | mocker.patch.object(module, "run", run_mock) 96 | 97 | try: 98 | module.main() 99 | except AnsibleRunEnd as e: 100 | return e.success, e.result 101 | assert False, "Module is not calling exit_json or fail_json." 102 | 103 | mocker.patch.multiple( 104 | basic.AnsibleModule, exit_json=exit_json_mock, fail_json=fail_json_mock 105 | ) 106 | return runner 107 | 108 | 109 | @pytest.fixture 110 | def run_main_with_reboot(mocker): 111 | def runner(module, params=None): 112 | args = dict( 113 | ANSIBLE_MODULE_ARGS=dict( 114 | _ansible_remote_tmp="/tmp", 115 | _ansible_keep_remote_files=False, 116 | ), 117 | ) 118 | args["ANSIBLE_MODULE_ARGS"].update(params or {}) 119 | mocker.patch.object(basic, "_ANSIBLE_ARGS", to_bytes(json.dumps(args))) 120 | 121 | # We can mock the run function because we enforce module structure in our 122 | # development guidelines. 123 | mocker.patch.object(module, "run", run_mock_with_reboot) 124 | 125 | try: 126 | module.main() 127 | except AnsibleRunEnd as e: 128 | return e.success, e.result 129 | assert False, "Module is not calling exit_json or fail_json." 130 | 131 | mocker.patch.multiple( 132 | basic.AnsibleModule, exit_json=exit_json_mock, fail_json=fail_json_mock 133 | ) 134 | return runner 135 | 136 | 137 | @pytest.fixture 138 | def run_main_info(mocker): 139 | def runner(module, params=None): 140 | args = dict( 141 | ANSIBLE_MODULE_ARGS=dict( 142 | _ansible_remote_tmp="/tmp", 143 | _ansible_keep_remote_files=False, 144 | ), 145 | ) 146 | args["ANSIBLE_MODULE_ARGS"].update(params or {}) 147 | mocker.patch.object(basic, "_ANSIBLE_ARGS", to_bytes(json.dumps(args))) 148 | 149 | # We can mock the run function because we enforce module structure in our 150 | # development guidelines. 151 | mocker.patch.object(module, "run", run_mock_info) 152 | 153 | try: 154 | module.main() 155 | except AnsibleRunEnd as e: 156 | return e.success, e.result 157 | assert False, "Module is not calling exit_json or fail_json." 158 | 159 | mocker.patch.multiple( 160 | basic.AnsibleModule, exit_json=exit_json_mock, fail_json=fail_json_mock 161 | ) 162 | return runner 163 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [base] 2 | lint_paths = plugins/ tests/unit/ 3 | 4 | [tox] 5 | minversion = 4 6 | skipsdist = True 7 | 8 | [testenv] 9 | deps = 10 | ansible-core==2.14.5 11 | pytest==7.1.2 12 | pytest-mock==3.8.2 13 | 14 | [testenv:coverage] 15 | deps = 16 | {[testenv]deps} 17 | coverage==6.5.0 18 | commands = 19 | -ansible-test coverage erase # On first run, there is nothing to erase. 20 | ansible-test units --venv --coverage 21 | ansible-test coverage html 22 | ansible-test coverage report --omit 'tests/*' --show-missing 23 | 24 | [testenv:docs] 25 | deps = 26 | ansible-core==2.14.5 27 | Sphinx 28 | sphinx-rtd-theme 29 | ansible-doc-extractor 30 | commands = 31 | # [[[cog 32 | # import cog 33 | # from pathlib import Path 34 | # for path in sorted(Path("plugins/modules").glob("*.py")): 35 | # cog.outl( 36 | # f"ansible-doc-extractor --template docs/templates/module.rst.j2 " 37 | # f"docs/source/modules {path}") 38 | # ]]] 39 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/block_device.py 40 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/block_device_info.py 41 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/boot_sources_info.py 42 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/dns_domain.py 43 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/dns_domain_info.py 44 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/dns_record.py 45 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/dns_record_info.py 46 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/fabric.py 47 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/fabric_info.py 48 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/instance.py 49 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/machine.py 50 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/machine_info.py 51 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/network_interface_info.py 52 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/network_interface_link.py 53 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/network_interface_physical.py 54 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/space.py 55 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/space_info.py 56 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/subnet.py 57 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/subnet_info.py 58 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/subnet_ip_range.py 59 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/subnet_ip_range_info.py 60 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/tag.py 61 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/tag_info.py 62 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/user.py 63 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/user_info.py 64 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/vlan.py 65 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/vlan_info.py 66 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/vm_host.py 67 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/vm_host_info.py 68 | ansible-doc-extractor --template docs/templates/module.rst.j2 docs/source/modules plugins/modules/vm_host_machine.py 69 | # [[[end]]] 70 | sphinx-build -M html docs/source docs/build 71 | 72 | [testenv:format] 73 | deps = 74 | cogapp 75 | black 76 | isort 77 | commands = 78 | isort {[base]lint_paths} 79 | black -q {[base]lint_paths} 80 | cog -r --verbosity=1 tox.ini 81 | 82 | [testenv:sanity] 83 | passenv = 84 | HOME 85 | deps = 86 | ansible-lint==6.22.1 87 | black 88 | flake8 89 | flake8-pyproject 90 | isort 91 | commands = 92 | isort --check-only --diff {[base]lint_paths} 93 | black --check {[base]lint_paths} 94 | flake8 {[base]lint_paths} 95 | ansible-lint 96 | ansible-test sanity --local --skip-test shebang 97 | 98 | [testenv:integration] 99 | passenv = 100 | HOME 101 | commands = 102 | ansible-test integration --requirements --local --diff {posargs} 103 | 104 | [testenv:units] 105 | passenv = 106 | HOME 107 | commands = 108 | ansible-test units --venv {posargs} 109 | -------------------------------------------------------------------------------- /tests/integration/targets/zzz_vm_host_post_tasks/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | # ----------------------------------Cleanup------------------------------------ 11 | - name: VM host cleanup 12 | maas.maas.vm_host: &delete-vm-hosts 13 | vm_host_name: "{{ item }}" 14 | state: absent 15 | loop: 16 | - lxd-test-updated 17 | 18 | - name: Machine cleanup 19 | maas.maas.machine: &delete-machine 20 | fqdn: "machine.{{ test_domain }}" 21 | state: absent 22 | 23 | # -----------------------------------Info------------------------------------ 24 | - name: List VM hosts 25 | maas.maas.vm_host_info: 26 | register: vm_hosts 27 | 28 | - name: List specific VM host 29 | maas.maas.vm_host_info: 30 | name: "{{ vm_hosts.records.0.name }}" 31 | register: vm_host 32 | - ansible.builtin.assert: 33 | that: 34 | - vm_host.records.0.name == vm_hosts.records.0.name 35 | 36 | # ------------------------------------Job------------------------------------ 37 | - name: Update LXD VM host 38 | maas.maas.vm_host: 39 | state: present 40 | vm_host_name: "{{ test_lxd_host.hostname }}" 41 | timeout: 180 42 | new_vm_host_name: lxd-test-updated 43 | cpu_over_commit_ratio: 2 44 | memory_over_commit_ratio: 3 45 | default_macvlan_mode: passthru 46 | tags: new-tag, new-tag2 47 | register: vm_host 48 | when: test_lxd_host is defined 49 | - ansible.builtin.assert: 50 | that: 51 | - vm_host is changed 52 | - vm_host.record.name == "lxd-test-updated" 53 | - vm_host.record.type == "lxd" 54 | - vm_host.record.cpu_over_commit_ratio == 2.0 55 | - vm_host.record.memory_over_commit_ratio == 3.0 56 | - vm_host.record.default_macvlan_mode == "passthru" 57 | # - vm_host.record.tags == ["new-tag", "new-tag2", "my-tag", "pod-console-logging"] this will work after tag bug is resolved 58 | when: test_lxd_host is defined 59 | 60 | - name: Update LXD VM host - idempotence 61 | maas.maas.vm_host: 62 | state: present 63 | vm_host_name: lxd-test-updated 64 | new_vm_host_name: lxd-test-updated 65 | cpu_over_commit_ratio: 2 66 | memory_over_commit_ratio: 3 67 | default_macvlan_mode: passthru 68 | register: vm_host 69 | when: test_lxd_host is defined 70 | - ansible.builtin.assert: 71 | that: 72 | - vm_host is not changed 73 | when: test_lxd_host is defined 74 | 75 | - name: Create virtual machine with two disks and a network interface 76 | maas.maas.vm_host_machine: 77 | hostname: machine 78 | vm_host: "{% if test_existing_vm_host is defined %}{{ test_existing_vm_host }}{% else %}lxd-test-updated{% endif %}" 79 | cores: 4 80 | memory: 8192 81 | network_interfaces: 82 | label_name: my_first 83 | subnet_cidr: "{{ test_subnet }}" 84 | storage_disks: 85 | - size_gigabytes: 8 86 | register: machine 87 | - ansible.builtin.assert: 88 | that: 89 | - machine is changed 90 | - machine.record.hostname == "machine" 91 | - machine.record.fqdn == "machine.{{ test_domain }}" 92 | - machine.record.memory == 8192 93 | - machine.record.cores == 4 94 | # TODO: update vm_host_machine to wait for ready state (otherwise task after will fail) 95 | 96 | - name: Register known already allocated machine as LXD VM host 97 | maas.maas.vm_host: 98 | state: present 99 | vm_host_name: machine 100 | machine_fqdn: "machine.{{ test_domain }}" 101 | timeout: 3600 102 | cpu_over_commit_ratio: 1 103 | memory_over_commit_ratio: 2 104 | default_macvlan_mode: bridge 105 | power_parameters: {} 106 | register: vm_host 107 | - ansible.builtin.assert: 108 | that: 109 | - vm_host is changed 110 | - vm_host.record.name == "machine" 111 | - vm_host.record.type == "lxd" 112 | - vm_host.record.cpu_over_commit_ratio == 1.0 113 | - vm_host.record.memory_over_commit_ratio == 2.0 114 | - vm_host.record.default_macvlan_mode == "bridge" 115 | 116 | - name: Delete VM host from already allocated machine 117 | maas.maas.vm_host: 118 | vm_host_name: machine 119 | state: absent 120 | register: vm_host 121 | - ansible.builtin.assert: 122 | that: 123 | - vm_host is changed 124 | 125 | - name: Delete VIRSH host 126 | maas.maas.vm_host: 127 | vm_host_name: "{{ test_virsh_host.hostname }}" 128 | state: absent 129 | register: vm_host 130 | when: test_virsh_host is defined 131 | - ansible.builtin.assert: 132 | that: 133 | - vm_host is changed 134 | when: test_virsh_host is defined 135 | 136 | - name: Delete LXD host 137 | maas.maas.vm_host: 138 | vm_host_name: lxd-test-updated 139 | state: absent 140 | register: vm_host 141 | when: test_lxd_host is defined 142 | - ansible.builtin.assert: 143 | that: 144 | - vm_host is changed 145 | when: test_lxd_host is defined 146 | 147 | # ----------------------------------Cleanup------------------------------------ 148 | - name: Remove VM host 149 | maas.maas.vm_host: *delete-vm-hosts 150 | loop: 151 | - lxd-test-updated 152 | 153 | - name: Delete machine 154 | maas.maas.machine: *delete-machine 155 | -------------------------------------------------------------------------------- /tests/integration/targets/vlan/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environment: 3 | MAAS_HOST: "{{ host }}" 4 | MAAS_TOKEN_KEY: "{{ token_key }}" 5 | MAAS_TOKEN_SECRET: "{{ token_secret }}" 6 | MAAS_CUSTOMER_KEY: "{{ customer_key }}" 7 | 8 | 9 | block: 10 | # ----------------------------------Cleanup------------------------------------ 11 | - name: List fabrics 12 | maas.maas.fabric_info: 13 | register: fabrics 14 | 15 | - name: Delete vlan (by vid) 16 | maas.maas.vlan: 17 | state: absent 18 | fabric_name: "{{ fabrics.records.0.name }}" 19 | vid: 5 20 | 21 | - name: Delete network space 22 | maas.maas.space: &delete-network-space 23 | name: space-for-vlan-test 24 | state: absent 25 | 26 | # -----------------------------------Info------------------------------------ 27 | - name: List vlans 28 | maas.maas.vlan_info: 29 | fabric_name: "{{ fabrics.records.0.name }}" 30 | register: vlans 31 | 32 | - name: List specific vlan 33 | maas.maas.vlan_info: 34 | fabric_name: "{{ fabrics.records.0.name }}" 35 | vlan_name: "{{ vlans.records.0.name }}" 36 | register: vlan 37 | - ansible.builtin.assert: 38 | that: 39 | - vlan.records.0.name == vlans.records.0.name 40 | 41 | # ------------------------------------Job------------------------------------ 42 | - name: Create vlan - missing parameter 43 | maas.maas.vlan: 44 | state: present 45 | fabric_name: fabric-10 46 | ignore_errors: true 47 | register: vlan 48 | - ansible.builtin.assert: 49 | that: 50 | - vlan is failed 51 | - "'one of the following is required: vid, vlan_name' in vlan.msg" 52 | 53 | - name: Add network space 54 | maas.maas.space: 55 | state: present 56 | name: space-for-vlan-test 57 | register: space 58 | - ansible.builtin.assert: 59 | that: 60 | - space is changed 61 | - space.record.name == "space-for-vlan-test" 62 | 63 | - name: Create vlan 64 | maas.maas.vlan: 65 | state: present 66 | fabric_name: "{{ fabrics.records.0.name }}" 67 | vid: 5 68 | vlan_name: vlan-5 69 | description: VLAN on fabric-0 70 | mtu: 1500 71 | dhcp_on: false 72 | space: "{{ space.record.name }}" 73 | register: vlan 74 | - ansible.builtin.assert: 75 | that: 76 | - vlan is changed 77 | - vlan.record.name == "vlan-5" 78 | - vlan.record.fabric == fabrics.records.0.name 79 | - vlan.record.mtu == 1500 80 | - vlan.record.dhcp_on == false 81 | - vlan.record.space == "space-for-vlan-test" 82 | # - vlan.record.description == "VLAN on fabric-0" # description is not returned 83 | 84 | - name: Create vlan - idempotence 85 | maas.maas.vlan: 86 | state: present 87 | fabric_name: "{{ fabrics.records.0.name }}" 88 | vid: 5 89 | vlan_name: vlan-5 90 | # description: VLAN on fabric-0 # description is not returned 91 | mtu: 1500 92 | dhcp_on: false 93 | space: "{{ space.record.name }}" 94 | register: vlan 95 | - ansible.builtin.assert: 96 | that: 97 | - vlan is not changed 98 | - vlan.record.name == "vlan-5" 99 | - vlan.record.fabric == fabrics.records.0.name 100 | - vlan.record.mtu == 1500 101 | - vlan.record.dhcp_on == false 102 | - vlan.record.space == "space-for-vlan-test" 103 | # - vlan.record.description == "VLAN on fabric-0" # description is not returned 104 | 105 | - name: Update vlan (by name) 106 | maas.maas.vlan: 107 | state: present 108 | fabric_name: "{{ fabrics.records.0.name }}" 109 | vlan_name: vlan-5 110 | new_vlan_name: vlan-555 111 | description: VLAN on fabric-0 updated 112 | mtu: 2000 113 | space: "" 114 | # dhcp_on: true # dhcp can only be turned on when a dynamic IP range is defined 115 | register: vlan 116 | - ansible.builtin.assert: 117 | that: 118 | - vlan is changed 119 | - vlan.record.name == "vlan-555" 120 | - vlan.record.fabric == fabrics.records.0.name 121 | - vlan.record.mtu == 2000 122 | # - vlan.record.dhcp_on == true 123 | - vlan.record.space == "undefined" 124 | # - vlan.record.description == "VLAN on fabric-0 updated" # description is not returned 125 | 126 | - name: Update vlan (by vid) - idempotence 127 | maas.maas.vlan: 128 | state: present 129 | fabric_name: "{{ fabrics.records.0.name }}" 130 | vid: 5 131 | new_vlan_name: vlan-555 132 | # description: VLAN on fabric-0 updated # description is not returned 133 | mtu: 2000 134 | space: "" 135 | # dhcp_on: true # dhcp can only be turned on when a dynamic IP range is defined 136 | register: vlan 137 | - ansible.builtin.assert: 138 | that: 139 | - vlan is changed 140 | - vlan.record.name == "vlan-555" 141 | - vlan.record.fabric == fabrics.records.0.name 142 | - vlan.record.mtu == 2000 143 | # - vlan.record.dhcp_on == true 144 | - vlan.record.space == "undefined" 145 | # - vlan.record.description == "VLAN on fabric-0 updated" # description is not returned 146 | 147 | # ----------------------------------Cleanup------------------------------------ 148 | - name: Delete vlan (by name) 149 | maas.maas.vlan: 150 | state: absent 151 | fabric_name: "{{ fabrics.records.0.name }}" 152 | vlan_name: vlan-555 153 | 154 | - name: Delete network space 155 | maas.maas.space: *delete-network-space 156 | -------------------------------------------------------------------------------- /plugins/modules/block_device_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright: (c) 2022, XLAB Steampunk 4 | # 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | module: block_device_info 13 | 14 | author: 15 | - Polona Mihalič (@PolonaM) 16 | short_description: Returns info about MAAS machines' block devices. 17 | description: 18 | - Plugin returns information about all block devices or specific block device if I(name) is provided. 19 | version_added: 1.0.0 20 | extends_documentation_fragment: 21 | - maas.maas.cluster_instance 22 | seealso: [] 23 | options: 24 | machine_fqdn: 25 | description: 26 | - Fully qualified domain name of the specific machine whose block devices will be listed. 27 | - Serves as unique identifier of the machine. 28 | - If machine is not found the task will FAIL. 29 | type: str 30 | required: true 31 | name: 32 | description: 33 | - Name of a block device. 34 | - Serves as unique identifier of the block device. 35 | - If block device is not found the task will FAIL. 36 | type: str 37 | """ 38 | 39 | EXAMPLES = r""" 40 | - name: Get list of all block devices of a selected machine. 41 | maas.maas.block_device_info: 42 | cluster_instance: 43 | host: host-ip 44 | token_key: token-key 45 | token_secret: token-secret 46 | customer_key: customer-key 47 | machine_fqdn: machine_name.project 48 | 49 | - name: Get info about a specific block device of a selected machine. 50 | maas.maas.block_device_info: 51 | cluster_instance: 52 | host: host-ip 53 | token_key: token-key 54 | token_secret: token-secret 55 | customer_key: customer-key 56 | machine_fqdn: machine_name.project 57 | name: newblockdevice 58 | """ 59 | 60 | RETURN = r""" 61 | records: 62 | description: 63 | - Machine's block device info list. 64 | returned: success 65 | type: list 66 | sample: 67 | - available_size: 0 68 | block_size: 512 69 | filesystem: null 70 | firmware_version: 2.5 71 | id: 146 72 | id_path: /dev/disk/by-id/scsi-SQEMU_QEMU_HARDDISK_lxd_root 73 | model: QEMU HARDDISK 74 | name: sda 75 | numa_node: 0 76 | partition_table_type: GPT 77 | partitions: 78 | - bootable: true 79 | device_id: 146 80 | filesystem: 81 | fstype: fat32 82 | label: efi 83 | mount_options: null 84 | mount_point: /boot/efi 85 | uuid: 42901672-60cb-44a3-bb8d-14f3314869c2 86 | id: 83 87 | path: /dev/disk/by-dname/sda-part1 88 | resource_uri: /MAAS/api/2.0/nodes/grydgf/blockdevices/146/partition/83 89 | size: 536870912 90 | system_id: grydgf 91 | tags: [] 92 | type: partition 93 | used_for: fat32 formatted filesystem mounted at /boot/efi 94 | uuid: 6f3259e0-7aba-442b-9c31-5b6bafb4796a 95 | - bootable: false 96 | device_id: 146 97 | filesystem: 98 | fstype: ext4 99 | label: root 100 | mount_options: null 101 | mount_point: / 102 | uuid: f74d9bc7-a2f1-4078-991c-68696794ee27 103 | id: 84 104 | path: /dev/disk/by-dname/sda-part2 105 | resource_uri: /MAAS/api/2.0/nodes/grydgf/blockdevices/146/partition/84 106 | size: 7457472512 107 | system_id: grydgf 108 | tags: [] 109 | type: partition 110 | used_for: ext4 formatted filesystem mounted at / 111 | uuid: eeba59a0-be8a-4be3-be15-e2858afa8487 112 | path: /dev/disk/by-dname/sda 113 | resource_uri: /MAAS/api/2.0/nodes/grydgf/blockdevices/146/ 114 | serial: lxd_root 115 | size: 8000004096 116 | storage_pool: default 117 | system_id: grydgf 118 | tags: 119 | rotary 120 | 1rpm 121 | type: physical 122 | used_for: GPT partitioned with 2 partitions 123 | used_size: 7999586304 124 | uuid: null 125 | """ 126 | 127 | from ansible.module_utils.basic import AnsibleModule 128 | 129 | from ..module_utils import arguments, errors 130 | from ..module_utils.block_device import BlockDevice 131 | from ..module_utils.client import Client 132 | from ..module_utils.machine import Machine 133 | 134 | 135 | def run(module, client: Client): 136 | machine = Machine.get_by_fqdn( 137 | module, client, must_exist=True, name_field_ansible="machine_fqdn" 138 | ) 139 | if module.params["name"]: 140 | block_device = BlockDevice.get_by_name( 141 | module, client, machine.id, must_exist=True 142 | ) 143 | response = [block_device.get(client)] 144 | else: 145 | response = client.get( 146 | f"/api/2.0/nodes/{machine.id}/blockdevices/" 147 | ).json 148 | return response 149 | 150 | 151 | def main(): 152 | module = AnsibleModule( 153 | supports_check_mode=True, 154 | argument_spec=dict( 155 | arguments.get_spec("cluster_instance"), 156 | machine_fqdn=dict(type="str", required=True), 157 | name=dict(type="str"), 158 | ), 159 | ) 160 | 161 | try: 162 | cluster_instance = module.params["cluster_instance"] 163 | host = cluster_instance["host"] 164 | consumer_key = cluster_instance["customer_key"] 165 | token_key = cluster_instance["token_key"] 166 | token_secret = cluster_instance["token_secret"] 167 | 168 | client = Client(host, token_key, token_secret, consumer_key) 169 | records = run(module, client) 170 | module.exit_json(changed=False, records=records) 171 | except errors.MaasError as e: 172 | module.fail_json(msg=str(e)) 173 | 174 | 175 | if __name__ == "__main__": 176 | main() 177 | -------------------------------------------------------------------------------- /plugins/module_utils/vlan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | from ..module_utils import errors 12 | from ..module_utils.client import Client 13 | from ..module_utils.rest_client import RestClient 14 | from ..module_utils.utils import MaasValueMapper, get_query 15 | 16 | 17 | class Vlan(MaasValueMapper): 18 | def __init__( 19 | self, 20 | name=None, 21 | id=None, 22 | vid=None, 23 | mtu=None, 24 | dhcp_on=None, 25 | external_dhcp=None, 26 | relay_vlan=None, 27 | space=None, 28 | fabric_id=None, 29 | secondary_rack=None, 30 | fabric=None, 31 | primary_rack=None, 32 | resource_uri=None, 33 | ): 34 | self.name = name 35 | self.id = id 36 | self.vid = vid 37 | self.mtu = mtu 38 | self.dhcp_on = dhcp_on 39 | self.external_dhcp = external_dhcp 40 | self.relay_vlan = relay_vlan 41 | self.space = space 42 | self.fabric_id = fabric_id 43 | self.secondary_rack = secondary_rack 44 | self.fabric = fabric 45 | self.primary_rack = primary_rack 46 | self.resource_uri = resource_uri 47 | 48 | @classmethod 49 | def get_by_name( 50 | cls, 51 | module, 52 | client: Client, 53 | fabric_id, 54 | must_exist=False, 55 | name_field_ansible="vlan_name", 56 | ): 57 | rest_client = RestClient(client=client) 58 | query = get_query( 59 | module, 60 | name_field_ansible, 61 | ansible_maas_map={name_field_ansible: "name"}, 62 | ) 63 | maas_dict = rest_client.get_record( 64 | f"/api/2.0/fabrics/{fabric_id}/vlans/", 65 | query, 66 | must_exist=must_exist, 67 | ) 68 | if maas_dict: 69 | vlan_from_maas = cls.from_maas(maas_dict) 70 | return vlan_from_maas 71 | 72 | @classmethod 73 | def get_by_vid(cls, vid, client: Client, fabric_id, must_exist=False): 74 | response = client.get(f"/api/2.0/fabrics/{fabric_id}/vlans/{vid}/") 75 | # Also possible: client.get(f"/api/2.0/vlans/{self.id}/").json 76 | if response.status == 404: 77 | if must_exist: 78 | raise errors.VlanNotFound(vid) 79 | return None 80 | vlan_maas_dict = response.json 81 | vlan = cls.from_maas(vlan_maas_dict) 82 | return vlan 83 | 84 | @classmethod 85 | def from_ansible(cls, module): 86 | return 87 | 88 | @classmethod 89 | def from_maas(cls, maas_dict): 90 | obj = cls() 91 | try: 92 | obj.name = maas_dict["name"] 93 | obj.id = maas_dict["id"] 94 | obj.vid = maas_dict["vid"] 95 | obj.mtu = maas_dict["mtu"] 96 | obj.dhcp_on = maas_dict["dhcp_on"] 97 | obj.external_dhcp = maas_dict["external_dhcp"] 98 | obj.relay_vlan = maas_dict["relay_vlan"] 99 | obj.space = maas_dict["space"] 100 | obj.fabric_id = maas_dict["fabric_id"] 101 | obj.secondary_rack = maas_dict["secondary_rack"] 102 | obj.fabric = maas_dict["fabric"] 103 | obj.primary_rack = maas_dict["primary_rack"] 104 | obj.resource_uri = maas_dict["resource_uri"] 105 | except KeyError as e: 106 | raise errors.MissingValueMAAS(e) 107 | return obj 108 | 109 | def to_maas(self): 110 | return 111 | 112 | def to_ansible(self): 113 | return dict( 114 | name=self.name, 115 | id=self.id, 116 | vid=self.vid, 117 | mtu=self.mtu, 118 | dhcp_on=self.dhcp_on, 119 | external_dhcp=self.external_dhcp, 120 | relay_vlan=self.relay_vlan, 121 | space=self.space, 122 | fabric_id=self.fabric_id, 123 | secondary_rack=self.secondary_rack, 124 | fabric=self.fabric, 125 | primary_rack=self.primary_rack, 126 | resource_uri=self.resource_uri, 127 | ) 128 | 129 | def delete(self, client): 130 | client.delete(f"/api/2.0/fabrics/{self.fabric_id}/vlans/{self.vid}/") 131 | 132 | def update(self, client, payload): 133 | return client.put( 134 | f"/api/2.0/fabrics/{self.fabric_id}/vlans/{self.vid}/", 135 | data=payload, 136 | ).json 137 | 138 | @classmethod 139 | def create(cls, client, fabric_id, payload): 140 | vlan_maas_dict = client.post( 141 | f"/api/2.0/fabrics/{fabric_id}/vlans/", 142 | data=payload, 143 | timeout=60, # Sometimes we get timeout error thus changing timeout from 20s to 60s 144 | ).json 145 | vlan = cls.from_maas(vlan_maas_dict) 146 | return vlan 147 | 148 | def __eq__(self, other): 149 | """One vlan is equal to another if it has all attributes exactly the same""" 150 | return all( 151 | ( 152 | self.name == other.name, 153 | self.id == other.id, 154 | self.vid == other.vid, 155 | self.mtu == other.mtu, 156 | self.dhcp_on == other.dhcp_on, 157 | self.external_dhcp == other.external_dhcp, 158 | self.relay_vlan == other.relay_vlan, 159 | self.space == other.space, 160 | self.fabric_id == other.fabric_id, 161 | self.secondary_rack == other.secondary_rack, 162 | self.fabric == other.fabric, 163 | self.primary_rack == other.primary_rack, 164 | self.resource_uri == other.resource_uri, 165 | ) 166 | ) 167 | --------------------------------------------------------------------------------