1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14DOCUMENTATION = r'''
15---
16module: mso_schema_template_contract_filter
17short_description: Manage contract filters in schema templates
18description:
19- Manage contract filters in schema templates on Cisco ACI Multi-Site.
20author:
21- Dag Wieers (@dagwieers)
22version_added: '2.8'
23options:
24  schema:
25    description:
26    - The name of the schema.
27    type: str
28    required: yes
29  template:
30    description:
31    - The name of the template.
32    type: str
33    required: yes
34  contract:
35    description:
36    - The name of the contract to manage.
37    type: str
38    required: yes
39  contract_display_name:
40    description:
41    - The name as displayed on the MSO web interface.
42    - This defaults to the contract name when unset on creation.
43    type: str
44  contract_filter_type:
45    description:
46    - The type of filters defined in this contract.
47    - This defaults to C(both-way) when unset on creation.
48    type: str
49    choices: [ both-way, one-way ]
50  contract_scope:
51    description:
52    - The scope of the contract.
53    - This defaults to C(vrf) when unset on creation.
54    type: str
55    choices: [ application-profile, global, tenant, vrf ]
56  filter:
57    description:
58    - The filter to associate with this contract.
59    type: str
60    aliases: [ name ]
61  filter_template:
62    description:
63    - The template name in which the filter is located.
64    type: str
65  filter_schema:
66    description:
67    - The schema name in which the filter is located.
68    type: str
69  filter_type:
70    description:
71    - The type of filter to manage.
72    type: str
73    choices: [ both-way, consumer-to-provider, provider-to-consumer ]
74    default: both-way
75    aliases: [ type ]
76  filter_directives:
77    description:
78    - A list of filter directives.
79    type: list
80    choices: [ log, none ]
81  state:
82    description:
83    - Use C(present) or C(absent) for adding or removing.
84    - Use C(query) for listing an object or multiple objects.
85    type: str
86    choices: [ absent, present, query ]
87    default: present
88seealso:
89- module: mso_schema_template_filter_entry
90notes:
91- Due to restrictions of the MSO REST API this module creates contracts when needed, and removes them when the last filter has been removed.
92- Due to restrictions of the MSO REST API concurrent modifications to contract filters can be dangerous and corrupt data.
93extends_documentation_fragment: mso
94'''
95
96EXAMPLES = r'''
97- name: Add a new contract filter
98  mso_schema_template_contract_filter:
99    host: mso_host
100    username: admin
101    password: SomeSecretPassword
102    schema: Schema 1
103    template: Template 1
104    contract: Contract 1
105    contract_scope: global
106    filter: Filter 1
107    state: present
108  delegate_to: localhost
109
110- name: Remove a contract filter
111  mso_schema_template_contract_filter:
112    host: mso_host
113    username: admin
114    password: SomeSecretPassword
115    schema: Schema 1
116    template: Template 1
117    contract: Contract 1
118    filter: Filter 1
119    state: absent
120  delegate_to: localhost
121
122- name: Query a specific contract filter
123  mso_schema_template_contract_filter:
124    host: mso_host
125    username: admin
126    password: SomeSecretPassword
127    schema: Schema 1
128    template: Template 1
129    contract: Contract 1
130    filter: Filter 1
131    state: query
132  delegate_to: localhost
133  register: query_result
134
135- name: Query all contract filters
136  mso_schema_template_contract_filter:
137    host: mso_host
138    username: admin
139    password: SomeSecretPassword
140    schema: Schema 1
141    template: Template 1
142    contract: Contract 1
143    state: query
144  delegate_to: localhost
145  register: query_result
146'''
147
148RETURN = r'''
149'''
150
151from ansible.module_utils.basic import AnsibleModule
152from ansible.module_utils.network.aci.mso import MSOModule, mso_argument_spec, mso_reference_spec, issubset
153
154FILTER_KEYS = {
155    'both-way': 'filterRelationships',
156    'consumer-to-provider': 'filterRelationshipsConsumerToProvider',
157    'provider-to-consumer': 'filterRelationshipsProviderToConsumer',
158}
159
160
161def main():
162    argument_spec = mso_argument_spec()
163    argument_spec.update(
164        schema=dict(type='str', required=True),
165        template=dict(type='str', required=True),
166        contract=dict(type='str', required=True),
167        contract_display_name=dict(type='str'),
168        contract_scope=dict(type='str', choices=['application-profile', 'global', 'tenant', 'vrf']),
169        contract_filter_type=dict(type='str', choices=['both-way', 'one-way']),
170        filter=dict(type='str', aliases=['name']),  # This parameter is not required for querying all objects
171        filter_directives=dict(type='list', choices=['log', 'none']),
172        filter_template=dict(type='str'),
173        filter_schema=dict(type='str'),
174        filter_type=dict(type='str', default='both-way', choices=FILTER_KEYS.keys(), aliases=['type']),
175        state=dict(type='str', default='present', choices=['absent', 'present', 'query']),
176    )
177
178    module = AnsibleModule(
179        argument_spec=argument_spec,
180        supports_check_mode=True,
181        required_if=[
182            ['state', 'absent', ['filter']],
183            ['state', 'present', ['filter']],
184        ],
185    )
186
187    schema = module.params['schema']
188    template = module.params['template']
189    contract = module.params['contract']
190    contract_display_name = module.params['contract_display_name']
191    contract_filter_type = module.params['contract_filter_type']
192    contract_scope = module.params['contract_scope']
193    filter_name = module.params['filter']
194    filter_directives = module.params['filter_directives']
195    filter_template = module.params['filter_template']
196    filter_schema = module.params['filter_schema']
197    filter_type = module.params['filter_type']
198    state = module.params['state']
199
200    contract_ftype = 'bothWay' if contract_filter_type == 'both-way' else 'oneWay'
201
202    if contract_filter_type == 'both-way' and filter_type != 'both-way':
203        module.warn("You are adding 'one-way' filters to a 'both-way' contract")
204    elif contract_filter_type != 'both-way' and filter_type == 'both-way':
205        module.warn("You are adding 'both-way' filters to a 'one-way' contract")
206
207    if filter_template is None:
208        filter_template = template
209
210    if filter_schema is None:
211        filter_schema = schema
212
213    filter_key = FILTER_KEYS[filter_type]
214
215    mso = MSOModule(module)
216
217    filter_schema_id = mso.lookup_schema(filter_schema)
218
219    # Get schema object
220    schema_obj = mso.get_obj('schemas', displayName=schema)
221    if schema_obj:
222        schema_id = schema_obj['id']
223    else:
224        mso.fail_json(msg="Provided schema '{0}' does not exist".format(schema))
225
226    schema_path = 'schemas/{id}'.format(**schema_obj)
227
228    # Get template
229    templates = [t['name'] for t in schema_obj['templates']]
230    if template not in templates:
231        mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ', '.join(templates)))
232    template_idx = templates.index(template)
233
234    # Get contracts
235    mso.existing = {}
236    contract_idx = None
237    filter_idx = None
238    contracts = [c['name'] for c in schema_obj['templates'][template_idx]['contracts']]
239
240    if contract in contracts:
241        contract_idx = contracts.index(contract)
242
243        filters = [f['filterRef'] for f in schema_obj['templates'][template_idx]['contracts'][contract_idx][filter_key]]
244        filter_ref = mso.filter_ref(schema_id=filter_schema_id, template=filter_template, filter=filter_name)
245        if filter_ref in filters:
246            filter_idx = filters.index(filter_ref)
247            filter_path = '/templates/{0}/contracts/{1}/{2}/{3}'.format(template, contract, filter_key, filter_name)
248            mso.existing = schema_obj['templates'][template_idx]['contracts'][contract_idx][filter_key][filter_idx]
249
250    if state == 'query':
251        if contract_idx is None:
252            mso.fail_json(msg="Provided contract '{0}' does not exist. Existing contracts: {1}".format(contract, ', '.join(contracts)))
253
254        if filter_name is None:
255            mso.existing = schema_obj['templates'][template_idx]['contracts'][contract_idx][filter_key]
256        elif not mso.existing:
257            mso.fail_json(msg="FilterRef '{filter_ref}' not found".format(filter_ref=filter_ref))
258        mso.exit_json()
259
260    ops = []
261    contract_path = '/templates/{0}/contracts/{1}'.format(template, contract)
262    filters_path = '/templates/{0}/contracts/{1}/{2}'.format(template, contract, filter_key)
263
264    mso.previous = mso.existing
265    if state == 'absent':
266        mso.proposed = mso.sent = {}
267
268        if contract_idx is None:
269            # There was no contract to begin with
270            pass
271        elif filter_idx is None:
272            # There was no filter to begin with
273            pass
274        elif len(filters) == 1:
275            # There is only one filter, remove contract
276            mso.existing = {}
277            ops.append(dict(op='remove', path=contract_path))
278        else:
279            # Remove filter
280            mso.existing = {}
281            ops.append(dict(op='remove', path=filter_path))
282
283    elif state == 'present':
284        if filter_directives is None:
285            filter_directives = ['none']
286
287        payload = dict(
288            filterRef=dict(
289                filterName=filter_name,
290                templateName=filter_template,
291                schemaId=filter_schema_id,
292            ),
293            directives=filter_directives,
294        )
295
296        mso.sanitize(payload, collate=True)
297        mso.existing = mso.sent
298
299        if contract_idx is None:
300            # Contract does not exist, so we have to create it
301            if contract_display_name is None:
302                contract_display_name = contract
303            if contract_filter_type is None:
304                contract_ftype = 'bothWay'
305            if contract_scope is None:
306                contract_scope = 'context'
307
308            payload = {
309                'name': contract,
310                'displayName': contract_display_name,
311                'filterType': contract_ftype,
312                'scope': contract_scope,
313            }
314
315            ops.append(dict(op='add', path='/templates/{0}/contracts/-'.format(template), value=payload))
316        else:
317            # Contract exists, but may require an update
318            if contract_display_name is not None:
319                ops.append(dict(op='replace', path=contract_path + '/displayName', value=contract_display_name))
320            if contract_filter_type is not None:
321                ops.append(dict(op='replace', path=contract_path + '/filterType', value=contract_ftype))
322            if contract_scope is not None:
323                ops.append(dict(op='replace', path=contract_path + '/scope', value=contract_scope))
324
325        if filter_idx is None:
326            # Filter does not exist, so we have to add it
327            ops.append(dict(op='add', path=filters_path + '/-', value=mso.sent))
328
329        else:
330            # Filter exists, we have to update it
331            ops.append(dict(op='replace', path=filter_path, value=mso.sent))
332
333    if not module.check_mode:
334        mso.request(schema_path, method='PATCH', data=ops)
335
336    mso.exit_json()
337
338
339if __name__ == "__main__":
340    main()
341