Skip to content

Commit 5e16381

Browse files
committed
Add support for new model format
* Modify to_argpsec filter to handle suboptions and conditional metadata (for eg: required_together, required_if etc) for suboptions * Add to_doc filter that renders the compelete module doc in Ansible module format * Additional changes in template file to work with new model format
1 parent 610f24b commit 5e16381

File tree

15 files changed

+249
-162
lines changed

15 files changed

+249
-162
lines changed

roles/init/tasks/main.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
file: "{{ model }}"
1616
name: rm
1717

18+
- name: Set the module documentation variable
19+
set_fact:
20+
rm_docmentation: "{{ rm['DOCUMENTATION']|from_yaml }}"
21+
22+
- name: Set the module ansible_metada variable
23+
set_fact:
24+
rm_ansible_metadata: "{{ rm['ANSIBLE_METADATA']|from_yaml }}"
25+
1826
- name: "Create the {{ structure }} directory structure"
1927
file:
2028
path: "{{ parent }}/{{ item }}"
@@ -23,7 +31,7 @@
2331

2432
- name: Copy the license file to the parent directory
2533
copy:
26-
src: "{{ rm['metadata']['license'] }}"
34+
src: "{{ rm['LICENSE'] | default('gpl-3.0.txt') }}"
2735
dest: "{{ parent }}/LICENSE.txt"
2836

2937
- name: Ensure the 'collection_org' is set when 'structure' is set to collection

roles/resource_module/filter_plugins/to_argspec.py

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,58 @@
1-
# Copyright (c) 2018 Ansible Project
1+
# Copyright (c) 2019 Ansible Project
22
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
33

44
from __future__ import (absolute_import, division, print_function)
5-
import jsonref
6-
import json
5+
__metaclass__ = type
6+
7+
import yaml
78
import pprint
9+
810
from ansible.module_utils.six import iteritems
11+
from ansible.module_utils.six import string_types
12+
from ansible.utils.display import Display
13+
from ansible.errors import AnsibleFilterError
914

10-
__metaclass__ = type
15+
OPTIONS_METADATA = ('type', 'elements', 'default', 'choices', 'required')
16+
SUBOPTIONS_METADATA = ('mutually_exclusive', 'required_together', 'required_one_of', 'supports_check_mode', 'required_if')
1117

12-
from ansible.errors import AnsibleFilterError
18+
display = Display()
19+
20+
21+
def retrieve_metadata(values, out):
22+
for key in OPTIONS_METADATA:
23+
if key in values:
24+
data = values.get(key, None)
25+
if data:
26+
out[key] = data
1327

14-
def dive(obj, required=False):
28+
29+
def dive(obj, result):
30+
for k, v in iteritems(obj):
31+
result[k] = dict()
32+
retrieve_metadata(v, result[k])
33+
suboptions = v.get('suboptions')
34+
if suboptions:
35+
for item in SUBOPTIONS_METADATA:
36+
if item in v:
37+
result[k][item] = v[item]
38+
result[k]['options'] = dict()
39+
dive(suboptions, result[k]['options'])
40+
41+
42+
def to_argspec(spec):
43+
if 'DOCUMENTATION' not in spec:
44+
raise AnsibleFilterError("missing required element 'DOCUMENTATION' in model")
45+
46+
if not isinstance(spec['DOCUMENTATION'], string_types):
47+
raise AnsibleFilterError("value of element 'DOCUMENTATION' should be of type string")
1548
result = {}
16-
if not 'type' in obj:
17-
raise AnsibleFilterError('missing type key')
18-
if obj['type'] == 'object':
19-
result['options'] = {}
20-
if not 'properties' in obj:
21-
raise AnsibleFilterError('missing properties key')
22-
for propkey, propval in iteritems(obj['properties']):
23-
required = bool('required' in obj and propkey in obj['required'])
24-
result['options'][propkey] = dive(propval, required)
25-
elif obj['type'] == 'array':
26-
result['options'] = {}
27-
if obj.get('elements'):
28-
result['elements'] = obj['elements']
29-
if not 'items' in obj:
30-
raise AnsibleFilterError('missing items key in array')
31-
if not 'properties' in obj['items']:
32-
raise AnsibleFilterError('missing properties in items')
33-
for propkey, propval in iteritems(obj['items']['properties']):
34-
required = bool('required' in obj['items'] and propkey in obj['items']['required'])
35-
result['options'][propkey] = dive(propval, required)
36-
result['type'] = 'list'
37-
elif obj['type'] in ['str', 'bool', 'int']:
38-
if 'default' in obj:
39-
result['default'] = obj['default']
40-
if 'enum' in obj:
41-
result['choices'] = obj['enum']
42-
if 'version_added' in obj:
43-
result['version_added'] = obj['version_added']
44-
result['required'] = required
45-
result['type'] = obj['type']
46-
return result
49+
doc = yaml.safe_load(spec['DOCUMENTATION'])
50+
51+
dive(doc['options'], result)
4752

48-
def to_argspec(value):
49-
data = jsonref.loads(json.dumps(value))
50-
result = dive(data['schema'])
51-
return str(result['options'])
53+
result = pprint.pformat(result, indent=1)
54+
display.debug("Arguments: %s" % result)
55+
return result
5256

5357

5458
class FilterModule(object):
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Copyright (c) 2019 Ansible Project
2+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3+
4+
from __future__ import (absolute_import, division, print_function)
5+
__metaclass__ = type
6+
7+
import os
8+
import shutil
9+
import json
10+
from subprocess import Popen, PIPE
11+
12+
from copy import deepcopy
13+
14+
from ansible.module_utils.six import StringIO, string_types
15+
from ansible.errors import AnsibleError, AnsibleFilterError
16+
from ansible.utils.display import Display
17+
from ansible.utils.path import unfrackpath, makedirs_safe
18+
19+
display = Display()
20+
21+
SECTIONS = ('ANSIBLE_METADATA', 'DOCUMENTATION', 'EXAMPLES', 'RETURN')
22+
23+
DOC_SECTION_SANITIZE = ('mutually_exclusive', 'required_together', 'required_one_of', 'supports_check_mode', 'required_if')
24+
25+
DEFAULT_RETURN = """
26+
before:
27+
description: The configuration prior to the model invocation.
28+
returned: always
29+
sample: The configuration returned will always be in the same format of the parameters above.
30+
after:
31+
description: The resulting configuration model invocation.
32+
returned: when changed
33+
sample: The configuration returned will always be in the same format of the parameters above.
34+
commands:
35+
description: The set of commands pushed to the remote device.
36+
returned: always
37+
type: list
38+
sample: ['command 1', 'command 2', 'command 3']
39+
"""
40+
41+
output = StringIO()
42+
RM_DIR_PATH = "~/.ansible/tmp/resource_model"
43+
44+
45+
def to_list(val):
46+
if isinstance(val, (list, tuple, set)):
47+
return list(val)
48+
elif val is not None:
49+
return [val]
50+
else:
51+
return list()
52+
53+
54+
def add(line, spaces=0, newline=True):
55+
line = line.rjust(len(line)+spaces, ' ')
56+
if newline:
57+
output.write(line + '\n')
58+
else:
59+
output.write(line)
60+
61+
62+
def get_ansible_metadata(spec, path):
63+
# write ansible metadata
64+
if 'ANSIBLE_METADATA' not in spec:
65+
raise AnsibleFilterError("missing required element 'ANSIBLE_METADATA' in model")
66+
67+
metadata = spec['ANSIBLE_METADATA']
68+
if not isinstance(metadata, string_types):
69+
raise AnsibleFilterError("value of element 'ANSIBLE_METADATA' should be of type string")
70+
71+
add('ANSIBLE_METADATA = %s' % metadata, newline=True)
72+
#add(metadata)
73+
74+
75+
def get_documentation(spec, path):
76+
# write documentation
77+
if 'DOCUMENTATION' not in spec:
78+
raise AnsibleFilterError("missing required element 'DOCUMENTATION' in model")
79+
80+
doc = spec['DOCUMENTATION']
81+
if not isinstance(doc, string_types):
82+
raise AnsibleFilterError("value of element 'DOCUMENTATION' should be of type string")
83+
84+
add('DOCUMENTATION = """')
85+
add('---')
86+
add('%s' % doc)
87+
add('"""')
88+
89+
90+
def get_examples(spec, path):
91+
# write examples
92+
if 'EXAMPLES' not in spec:
93+
raise AnsibleFilterError("missing required element 'EXAMPLES' in model")
94+
95+
add('EXAMPLES = """')
96+
dir_name = os.path.dirname(path)
97+
for item in to_list(spec['EXAMPLES']):
98+
with open(os.path.join(dir_name, item)) as fp:
99+
add(fp.read().strip("\n"))
100+
add("\n")
101+
add('"""')
102+
103+
104+
def get_return(spec, path):
105+
# write return
106+
ret = spec.get('RETURN')
107+
add('RETURN = """')
108+
add(ret) if ret else add(DEFAULT_RETURN.strip())
109+
add('"""')
110+
111+
112+
def validate_model(model, contents):
113+
try:
114+
resource_module_dir = unfrackpath(RM_DIR_PATH)
115+
makedirs_safe(resource_module_dir)
116+
module_name = "%s_%s" % (model['NETWORK_OS'], model['RESOURCE'])
117+
module_file_path = os.path.join(RM_DIR_PATH, '%s.%s' % (module_name, 'py'))
118+
module_file_path = os.path.realpath(os.path.expanduser(module_file_path))
119+
with open(module_file_path, 'w+') as fp:
120+
fp.write(contents)
121+
122+
display.debug("Module file: %s" % module_file_path)
123+
124+
# validate the model
125+
cmd = ["ansible-doc", "-M", os.path.dirname(module_file_path), module_name]
126+
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
127+
out, err = p.communicate()
128+
if err:
129+
raise AnsibleError("Error while parsing module: %s" % err)
130+
display.debug("Module output:\n%s" % out)
131+
except Exception as e:
132+
raise AnsibleError('Failed to validate the model with error: %s\n%s' % (e, contents))
133+
finally:
134+
shutil.rmtree(os.path.realpath(os.path.expanduser(resource_module_dir)), ignore_errors=True)
135+
136+
137+
def _sanitize_documentation(doc):
138+
sanitize_doc =[]
139+
for line in doc.splitlines():
140+
for item in DOC_SECTION_SANITIZE:
141+
if line.strip().startswith(item):
142+
break
143+
else:
144+
sanitize_doc.append(line)
145+
return "\n".join(sanitize_doc)
146+
147+
148+
def to_doc(rm, path):
149+
model = deepcopy(rm)
150+
model['DOCUMENTATION'] = _sanitize_documentation(rm['DOCUMENTATION'])
151+
path = os.path.realpath(os.path.expanduser(path))
152+
if not os.path.isfile(path):
153+
raise AnsibleFilterError("model file %s does not exist" % path)
154+
155+
for name in SECTIONS:
156+
func = globals().get('get_%s' % name.lower())
157+
func(model, path)
158+
159+
contents = output.getvalue()
160+
display.debug("%s" % contents)
161+
validate_model(model, contents)
162+
163+
return contents
164+
165+
166+
class FilterModule(object):
167+
def filters(self):
168+
return {
169+
'to_doc': to_doc,
170+
}

roles/resource_module/filter_plugins/to_docoptions.py

Lines changed: 0 additions & 63 deletions
This file was deleted.

roles/resource_module/templates/module_directory/network_os/network_os_facts.py.j2

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/python
22
# -*- coding: utf-8 -*-
3-
# {{ rm['metadata']['copyright_str'] }}
3+
# {{ rm['COPYRIGHT'] }}
44
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
55
"""
66
The module file for {{ network_os }}_facts
@@ -12,23 +12,23 @@ from ansible.module_utils.connection import Connection
1212
from {{ import_path }}. \
1313
{{ network_os }}.facts.facts import Facts
1414

15-
ANSIBLE_METADATA = {'metadata_version': '1.1',
16-
'status': {{ rm['metadata']['status'] }},
17-
'supported_by': {{ rm['metadata']['supported_by'] }}}
15+
ANSIBLE_METADATA = {'metadata_version': '{{rm_ansible_metadata['metadata_version']}}',
16+
'status': {{rm_ansible_metadata['status']}},
17+
'supported_by': '{{rm_ansible_metadata['supported_by']}}',}
1818

1919

2020
DOCUMENTATION = """
2121
---
2222
module: {{ network_os }}_facts
23-
version_added: {{ rm['info']['version_added'] }}
23+
version_added: {{ rm_docmentation['version_added'] }}
2424
short_description: Get facts about {{ network_os }} devices.
2525
description:
2626
- Collects facts from network devices running the {{ network_os }} operating
2727
system. This module places the facts gathered in the fact tree keyed by the
2828
respective resource name. The facts module will always collect a
2929
base set of facts from the device and can enable or disable
3030
collection of additional facts.
31-
author: {{ rm['info']['authors'] }}
31+
author: {{ rm_docmentation['author'] }}
3232
options:
3333
gather_subset:
3434
description:

0 commit comments

Comments
 (0)