From 566760042039be43a897fe5785e9c0b63c9fe6e0 Mon Sep 17 00:00:00 2001 From: Sagi Shnaidman Date: Tue, 12 May 2020 17:52:48 +0300 Subject: [PATCH] Add template for generation of artibtrary module One is for resource changing module, like server start or delete, second one is for info collection about a specific resource. Change-Id: I78b35075111731fff2fd50837fa4e6e0c61c55a0 --- contrib/generate_module.sh | 5 + contrib/module_info_template.py.j2 | 110 +++++++++++++++++++++ contrib/module_template.py.j2 | 149 +++++++++++++++++++++++++++++ contrib/module_template_vars.yaml | 81 ++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100755 contrib/generate_module.sh create mode 100644 contrib/module_info_template.py.j2 create mode 100644 contrib/module_template.py.j2 create mode 100644 contrib/module_template_vars.yaml diff --git a/contrib/generate_module.sh b/contrib/generate_module.sh new file mode 100755 index 00000000..abec2c30 --- /dev/null +++ b/contrib/generate_module.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# For resource changing module +ansible localhost -c local -m template -a "src=module_template.py.j2 dest=my_module.py" -e @module_template_vars.yaml +# For resource info collection module +ansible localhost -c local -m template -a "src=module_info_template.py.j2 dest=my_module_info.py" -e @module_template_vars.yaml diff --git a/contrib/module_info_template.py.j2 b/contrib/module_info_template.py.j2 new file mode 100644 index 00000000..da646ecb --- /dev/null +++ b/contrib/module_info_template.py.j2 @@ -0,0 +1,110 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, {{ author_name }} <{{ author_mail }}> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: {{ module_name }} +short_description: {{ module_short_description }} +author: {{ author_name }} <{{ author_mail }}> +description: + - {{ module_long_description }} +options: + {{ options|to_nice_yaml(indent=2,sort_keys=false)|indent(width=2)|trim }} + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +RETURN = ''' +{{ module_returns_example|to_nice_yaml(indent=2,sort_keys=false) }} +''' + +EXAMPLES = ''' +# What modules does for example +- {{ module_name }}: + name: + - name1 + - name2 + timeout: 200 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class {{ module_name.split("_")|map("capitalize")|list|join("") }}Module(OpenStackModule): + + argument_spec = dict( +{% for k, v in options.items() %} +{{ k | indent( width=8, indentfirst=True) }}=dict(type={{ v.type }} +{%- if 'required' in v %}, required={{ v.required }}{% endif %} +{%- if 'elements' in v %}, elements={{ v.elements }}{% endif %} +{%- if 'default' in v %}, default={% if v.type == 'str' %}"{{ v.default }}"{% else %}{{ v.default }}{% endif %}{% endif %} +{%- if 'aliases' in v %}, aliases={{ v.aliases }}{% endif %} +{%- if 'choices' in v %}, choices={{ v.choices }}{% endif %}), +{% endfor %} + ), + + # Optional arguments requirements + module_kwargs = dict( + required_if=[ + ['action', 'rebuild', ['image']], # if need to rebuild image (only), the 'image' is required + ["state", "present", ["username", "user_roles"]], # for creating user 'user_roles' is required + ["state", "absent", ["username"]], # for state 'absent' only username is required + ], + required_by=dict( # for weather and population 'city' is required to set + weather=('city'), + population=('city'), + ), + mutually_exclusive=[ + ['use_cloud1', 'use_cloud2'] # can't run on both, choose only one to set + ], + required_together=[ + ['remove_image', 'image_name'] # if need to remove image, must to specify which one + ], + required_one_of_args=[["password", "password_hash"]], # one of these args must be set + supports_check_mode={{ check_mode_support }}, # good practice is to support check_mode + ) + + # you main funciton is here + def run(self): + # do any arguments check if needed + data = self.preliminary_checks() + # check if we need to prepare various filters for results + filters = self.prepare_filters() + # run SDK call to get information about requested resource + result = self.conn.compute.resource_list( + filters=filters, + detailed=self.params['detailed'], + # any other parameters + ) + # process results if they require a change + result = self.normalize_result() + self.results.update({'resource_name': result}) + + def preliminary_checks(self): + # you checks before running like arguments and options checks, etc + return data + + def prepare_filters(self): + # process filters if they require additional checks + return filters + + def normalize_result(self): + # process filters if they require additional checks + return result + + +def main(): + module = {{ module_name.split("_")|map("capitalize")|list|join("") }}Module() + module() + + +if __name__ == '__main__': + main() diff --git a/contrib/module_template.py.j2 b/contrib/module_template.py.j2 new file mode 100644 index 00000000..44d76606 --- /dev/null +++ b/contrib/module_template.py.j2 @@ -0,0 +1,149 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, {{ author_name }} <{{ author_mail }}> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: {{ module_name }} +short_description: {{ module_short_description }} +author: {{ author_name }} <{{ author_mail }}> +description: + - {{ module_long_description }} +options: + {{ options|to_nice_yaml(indent=2,sort_keys=false)|indent(width=2)|trim }} + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +RETURN = ''' +{{ module_returns_example|to_nice_yaml(indent=2,sort_keys=false) }} +''' + +EXAMPLES = ''' +# What modules does for example +- {{ module_name }}: + action: pause + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + server: vm1 + timeout: 200 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class {{ module_name.split("_")|map("capitalize")|list|join("") }}Module(OpenStackModule): + + argument_spec = dict( +{% for k, v in options.items() %} +{{ k | indent( width=8, indentfirst=True) }}=dict(type={{ v.type }} +{%- if 'required' in v %}, required={{ v.required }}{% endif %} +{%- if 'elements' in v %}, elements={{ v.elements }}{% endif %} +{%- if 'default' in v %}, default={% if v.type == 'str' %}"{{ v.default }}"{% else %}{{ v.default }}{% endif %}{% endif %} +{%- if 'aliases' in v %}, aliases={{ v.aliases }}{% endif %} +{%- if 'choices' in v %}, choices={{ v.choices }}{% endif %}), +{% endfor %} + ), + + # Optional arguments requirements + module_kwargs = dict( + required_if=[ + ['action', 'rebuild', ['image']], # if need to rebuild image (only), the 'image' is required + ["state", "present", ["username", "user_roles"]], # for creating user 'user_roles' is required + ["state", "absent", ["username"]], # for state 'absent' only username is required + ], + required_by=dict( # for weather and population 'city' is required to set + weather=('city'), + population=('city'), + ), + mutually_exclusive=[ + ['use_cloud1', 'use_cloud2'] # can't run on both, choose only one to set + ], + required_together=[ + ['remove_image', 'image_name'] # if need to remove image, must to specify which one + ], + required_one_of_args=[["password", "password_hash"]], # one of these args must be set + supports_check_mode={{ check_mode_support }}, # good practice is to support check_mode + ) + + # you main funciton is here + def run(self): + # do any arguments check if needed + data = self.preliminary_checks() + # check if we need to run or the resource is in desired state already + must_run = self.check_mode_test() + # if the resource is good + if not must_run: + # updated returned results if need + self.results.update({"returning_data": some_data}) + # returning {changed: False, ...} because we didn't change resource + self.exit_json(self.results) + # do something if must to run the module + self.execute() + + def preliminary_checks(self): + # you checks before running like arguments and options checks, etc + return data + + def check_mode_test(self): + # check the idempotency - does module should do anything or + # it's already in the desired state? + return must_run + + def execute(self): + # doing here what should be done, using OpenstackSDK + # for example actions for resource: + # self.params['action'] = "rebuild" + action_name = self.params['action'] + "_resource" # action_name='rebuild_resource' + try: + # find a method "rebuild_resource" in openstack SDK compute: + func_name = getattr(self.conn.compute, action_name) + # found self.conn.compute.rebuild_resource + except AttributeError: + self.fail_json( + msg="Method %s wasn't found in OpenstackSDK compute" % action_name) + summary = func_name(data) # summary = self.conn.compute.rebuild_resource(data) + self.results.update({"returning_data": summary}) + # that's it, exiting, results will be returned from module automatically + + # another option for states + def execute_with_action_map(self): + actions_map = { + 'start': self._start_resource, + 'stop': self._stop_resource, + 'restart': self._restart_resource, + 'absent': self._absent_resource, + } + summary = actions_map(self.params['action'])() # summary = self.start_resource() + self.results.update({"changed": True, "data2return": summary}) + + def _start_resource(self, some_other_data): + pass + + def _stop_resource(self, some_other_data): + pass + + def _restart_resource(self, some_other_data): + pass + + def _absent_resource(self, some_other_data): + pass + + +def main(): + module = {{ module_name.split("_")|map("capitalize")|list|join("") }}Module() + module() + + +if __name__ == '__main__': + main() diff --git a/contrib/module_template_vars.yaml b/contrib/module_template_vars.yaml new file mode 100644 index 00000000..83578776 --- /dev/null +++ b/contrib/module_template_vars.yaml @@ -0,0 +1,81 @@ +##### PLEASE READ BEFORE ##### + +# Module format and documentation +# https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html#module-format-and-documentation + + +module_name: server_manage +author_name: 'Happy Ansible User' +author_mail: dontwriteme@example.com +module_short_description: "Doing something very useful" +module_long_description: "Here is the place to release your inner writer" +check_mode_support: True # good practice to support check_mode: +# https://docs.ansible.com/ansible/latest/user_guide/playbooks_checkmode.html#check-mode-dry-run + +module_returns_example: + image: + description: Image inspection results for the image that was pulled, pushed, or built. + returned: always # or 'success' in case of success only + type: dict + sample: + Image Name: Sample Image + Image ID: e6471d00796a13de8142c15d7ad3a44f + Nested: + images list: + - data 1 + - image 1234 + boolean_1: True + +options: + optional_string: + description: + - This variable is set for having string argument, for example 'action' + type: str + required: true + default: "my_lovely_action" + choices: + - allowed_option1 + - allowed_option1 + optional_boolean: + description: + - This variable is set for having a boolean argument, for example whether + to wait for resource creation or not + type: bool + required: false # may be omitted if false + # and no default because not required + optional_integer: + description: + - This variable is set for having a integer argument, for example how many + seconds to wait for the resource to come alive + required: true + default: 60 + type: int + aliases: # sometimes we allow to pass the same option with different name + - old_optional_integer_name + - different_option_name + optional_list: + description: + - This variable is set for having a list argument, for example files need + to create with the resource + type: list + elements: str # type of elements of the list, can be dict, str, int, list + optional_dictionary: + description: + - This variable is set for having a dictionary argument, for example to + set environment variables or to pass more complex data to SDK + required: true + default: {} + type: dict + suboptions: + suboption_1: + description: + - suboption_1 description, what it does + type: str + aliases: + - suboption_1_another_name + suboption_2: + description: + - suboption_2 description, what it does + type: list + elements: str + default: []