1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com>
5# Copyright: (c) 2020, Lionel Hercot (@lhercot) <lhercot@cisco.com>
6# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'community'}
14
15DOCUMENTATION = r'''
16---
17module: mso_schema_site_vrf_region_cidr
18short_description: Manage site-local VRF region CIDRs in schema template
19description:
20- Manage site-local VRF region CIDRs in schema template on Cisco ACI Multi-Site.
21author:
22- Dag Wieers (@dagwieers)
23- Lionel Hercot (@lhercot)
24options:
25  schema:
26    description:
27    - The name of the schema.
28    type: str
29    required: yes
30  site:
31    description:
32    - The name of the site.
33    type: str
34    required: yes
35  template:
36    description:
37    - The name of the template.
38    type: str
39    required: yes
40  vrf:
41    description:
42    - The name of the VRF.
43    type: str
44    required: yes
45  region:
46    description:
47    - The name of the region.
48    type: str
49    required: yes
50  cidr:
51    description:
52    - The name of the region CIDR to manage.
53    type: str
54    aliases: [ ip ]
55  primary:
56    description:
57    - Whether this is the primary CIDR.
58    type: bool
59    default: true
60  state:
61    description:
62    - Use C(present) or C(absent) for adding or removing.
63    - Use C(query) for listing an object or multiple objects.
64    type: str
65    choices: [ absent, present, query ]
66    default: present
67notes:
68- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index.
69  This can cause silent corruption on concurrent access when changing/removing on object as
70  the wrong object may be referenced. This module is affected by this deficiency.
71seealso:
72- module: cisco.mso.mso_schema_site_vrf_region
73- module: cisco.mso.mso_schema_site_vrf_region_cidr_subnet
74- module: cisco.mso.mso_schema_template_vrf
75extends_documentation_fragment: cisco.mso.modules
76'''
77
78EXAMPLES = r'''
79- name: Add a new site VRF region CIDR
80  cisco.mso.mso_schema_site_vrf_region_cidr:
81    host: mso_host
82    username: admin
83    password: SomeSecretPassword
84    schema: Schema1
85    site: Site1
86    template: Template1
87    vrf: VRF1
88    region: us-west-1
89    cidr: 14.14.14.1/24
90    state: present
91  delegate_to: localhost
92
93- name: Remove a site VRF region CIDR
94  cisco.mso.mso_schema_site_vrf_region_cidr:
95    host: mso_host
96    username: admin
97    password: SomeSecretPassword
98    schema: Schema1
99    site: Site1
100    template: Template1
101    vrf: VRF1
102    region: us-west-1
103    cidr: 14.14.14.1/24
104    state: absent
105  delegate_to: localhost
106
107- name: Query a specific site VRF region CIDR
108  cisco.mso.mso_schema_site_vrf_region_cidr:
109    host: mso_host
110    username: admin
111    password: SomeSecretPassword
112    schema: Schema1
113    site: Site1
114    template: Template1
115    vrf: VRF1
116    region: us-west-1
117    cidr: 14.14.14.1/24
118    state: query
119  delegate_to: localhost
120  register: query_result
121
122- name: Query all site VRF region CIDR
123  cisco.mso.mso_schema_site_vrf_region_cidr:
124    host: mso_host
125    username: admin
126    password: SomeSecretPassword
127    schema: Schema1
128    site: Site1
129    template: Template1
130    vrf: VRF1
131    region: us-west-1
132    state: query
133  delegate_to: localhost
134  register: query_result
135'''
136
137RETURN = r'''
138'''
139
140from ansible.module_utils.basic import AnsibleModule
141from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec
142
143
144def main():
145    argument_spec = mso_argument_spec()
146    argument_spec.update(
147        schema=dict(type='str', required=True),
148        site=dict(type='str', required=True),
149        template=dict(type='str', required=True),
150        vrf=dict(type='str', required=True),
151        region=dict(type='str', required=True),
152        cidr=dict(type='str', aliases=['ip']),  # This parameter is not required for querying all objects
153        primary=dict(type='bool', default=True),
154        state=dict(type='str', default='present', choices=['absent', 'present', 'query']),
155    )
156
157    module = AnsibleModule(
158        argument_spec=argument_spec,
159        supports_check_mode=True,
160        required_if=[
161            ['state', 'absent', ['cidr']],
162            ['state', 'present', ['cidr']],
163        ],
164    )
165
166    schema = module.params.get('schema')
167    site = module.params.get('site')
168    template = module.params.get('template').replace(' ', '')
169    vrf = module.params.get('vrf')
170    region = module.params.get('region')
171    cidr = module.params.get('cidr')
172    primary = module.params.get('primary')
173    state = module.params.get('state')
174
175    mso = MSOModule(module)
176
177    # Get schema objects
178    schema_id, schema_path, schema_obj = mso.query_schema(schema)
179
180    # Get template
181    templates = [t.get('name') for t in schema_obj.get('templates')]
182    if template not in templates:
183        mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ', '.join(templates)))
184    template_idx = templates.index(template)
185
186    payload = dict()
187    op_path = ''
188    new_cidr = dict(
189        ip=cidr,
190        primary=primary,
191    )
192
193    # Get site
194    site_id = mso.lookup_site(site)
195
196    # Get site_idx
197    all_sites = schema_obj.get('sites')
198    sites = []
199    if all_sites is not None:
200        sites = [(s.get('siteId'), s.get('templateName')) for s in all_sites]
201
202    # Get VRF
203    vrf_ref = mso.vrf_ref(schema_id=schema_id, template=template, vrf=vrf)
204    template_vrfs = [a.get('name') for a in schema_obj['templates'][template_idx]['vrfs']]
205    if vrf not in template_vrfs:
206        mso.fail_json(msg="Provided vrf '{0}' does not exist. Existing vrfs: {1}".format(vrf, ', '.join(template_vrfs)))
207
208    # if site-template does not exist, create it
209    if (site_id, template) not in sites:
210        op_path = '/sites/-'
211        payload.update(
212            siteId=site_id,
213            templateName=template,
214            vrfs=[dict(
215                vrfRef=dict(
216                    schemaId=schema_id,
217                    templateName=template,
218                    vrfName=vrf,
219                ),
220                regions=[dict(
221                    name=region,
222                    cidrs=[new_cidr]
223                )]
224            )]
225        )
226
227    else:
228        # Schema-access uses indexes
229        site_idx = sites.index((site_id, template))
230        # Path-based access uses site_id-template
231        site_template = '{0}-{1}'.format(site_id, template)
232
233        # If vrf not at site level but exists at template level
234        vrfs = [v.get('vrfRef') for v in schema_obj.get('sites')[site_idx]['vrfs']]
235        if vrf_ref not in vrfs:
236            op_path = '/sites/{0}/vrfs/-'.format(site_template)
237            payload.update(
238                vrfRef=dict(
239                    schemaId=schema_id,
240                    templateName=template,
241                    vrfName=vrf,
242                ),
243                regions=[dict(
244                    name=region,
245                    cidrs=[new_cidr]
246                )]
247            )
248        else:
249            # Update vrf index at site level
250            vrf_idx = vrfs.index(vrf_ref)
251
252            # Get Region
253            regions = [r.get('name') for r in schema_obj.get('sites')[site_idx]['vrfs'][vrf_idx]['regions']]
254            if region not in regions:
255                op_path = '/sites/{0}/vrfs/{1}/regions/-'.format(site_template, vrf)
256                payload.update(
257                    name=region,
258                    cidrs=[new_cidr]
259                )
260            else:
261                region_idx = regions.index(region)
262
263                # Get CIDR
264                cidrs = [c.get('ip') for c in schema_obj.get('sites')[site_idx]['vrfs'][vrf_idx]['regions'][region_idx]['cidrs']]
265                if cidr is not None:
266                    if cidr in cidrs:
267                        cidr_idx = cidrs.index(cidr)
268                        # FIXME: Changes based on index are DANGEROUS
269                        cidr_path = '/sites/{0}/vrfs/{1}/regions/{2}/cidrs/{3}'.format(site_template, vrf, region, cidr_idx)
270                        mso.existing = schema_obj.get('sites')[site_idx]['vrfs'][vrf_idx]['regions'][region_idx]['cidrs'][cidr_idx]
271                    op_path = '/sites/{0}/vrfs/{1}/regions/{2}/cidrs/-'.format(site_template, vrf, region)
272                    payload = new_cidr
273
274    if state == 'query':
275        if (site_id, template) not in sites:
276            mso.fail_json(msg="Provided site-template association '{0}-{1}' does not exist.".format(site, template))
277        elif vrf_ref not in vrfs:
278            mso.fail_json(msg="Provided vrf '{0}' does not exist at site level.".format(vrf))
279        elif not regions or region not in regions:
280            mso.fail_json(msg="Provided region '{0}' does not exist. Existing regions: {1}".format(region, ', '.join(regions)))
281        elif cidr is None and not payload:
282            mso.existing = schema_obj.get('sites')[site_idx]['vrfs'][vrf_idx]['regions'][region_idx]['cidrs']
283        elif not mso.existing:
284            mso.fail_json(msg="CIDR IP '{cidr}' not found".format(cidr=cidr))
285        mso.exit_json()
286
287    ops = []
288
289    mso.previous = mso.existing
290    if state == 'absent':
291        if mso.existing:
292            mso.sent = mso.existing = {}
293            ops.append(dict(op='remove', path=cidr_path))
294
295    elif state == 'present':
296        mso.sanitize(payload, collate=True)
297
298        if mso.existing:
299            ops.append(dict(op='replace', path=cidr_path, value=mso.sent))
300        else:
301            ops.append(dict(op='add', path=op_path, value=mso.sent))
302
303        mso.existing = new_cidr
304
305    if not module.check_mode and mso.previous != mso.existing:
306        mso.request(schema_path, method='PATCH', data=ops)
307
308    mso.exit_json()
309
310
311if __name__ == "__main__":
312    main()
313