1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com>
5# GNU General Public License v3.0+ (see LICENSE 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_site_anp_epg_staticleaf
17short_description: Manage site-local EPG static leafs in schema template
18description:
19- Manage site-local EPG static leafs in schema template on Cisco ACI Multi-Site.
20author:
21- Dag Wieers (@dagwieers)
22options:
23  schema:
24    description:
25    - The name of the schema.
26    type: str
27    required: yes
28  site:
29    description:
30    - The name of the site.
31    type: str
32    required: yes
33  template:
34    description:
35    - The name of the template.
36    type: str
37    required: yes
38  anp:
39    description:
40    - The name of the ANP.
41    type: str
42    required: yes
43  epg:
44    description:
45    - The name of the EPG.
46    type: str
47    required: yes
48  pod:
49    description:
50    - The pod of the static leaf.
51    type: str
52  leaf:
53    description:
54    - The path of the static leaf.
55    type: str
56    aliases: [ name ]
57  vlan:
58    description:
59    - The VLAN id of the static leaf.
60    type: int
61  state:
62    description:
63    - Use C(present) or C(absent) for adding or removing.
64    - Use C(query) for listing an object or multiple objects.
65    type: str
66    choices: [ absent, present, query ]
67    default: present
68notes:
69- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index.
70  This can cause silent corruption on concurrent access when changing/removing on object as
71  the wrong object may be referenced. This module is affected by this deficiency.
72seealso:
73- module: cisco.mso.mso_schema_site_anp_epg
74- module: cisco.mso.mso_schema_template_anp_epg
75extends_documentation_fragment: cisco.mso.modules
76'''
77
78EXAMPLES = r'''
79- name: Add a new static leaf to a site EPG
80  cisco.mso.mso_schema_site_anp_epg_staticleaf:
81    host: mso_host
82    username: admin
83    password: SomeSecretPassword
84    schema: Schema1
85    site: Site1
86    template: Template1
87    anp: ANP1
88    epg: EPG1
89    leaf: Leaf1
90    vlan: 123
91    state: present
92  delegate_to: localhost
93
94- name: Remove a static leaf from a site EPG
95  cisco.mso.mso_schema_site_anp_epg_staticleaf:
96    host: mso_host
97    username: admin
98    password: SomeSecretPassword
99    schema: Schema1
100    site: Site1
101    template: Template1
102    anp: ANP1
103    epg: EPG1
104    leaf: Leaf1
105    state: absent
106  delegate_to: localhost
107
108- name: Query a specific site EPG static leaf
109  cisco.mso.mso_schema_site_anp_epg_staticleaf:
110    host: mso_host
111    username: admin
112    password: SomeSecretPassword
113    schema: Schema1
114    site: Site1
115    template: Template1
116    anp: ANP1
117    epg: EPG1
118    leaf: Leaf1
119    state: query
120  delegate_to: localhost
121  register: query_result
122
123- name: Query all site EPG static leafs
124  cisco.mso.mso_schema_site_anp_epg_staticleaf:
125    host: mso_host
126    username: admin
127    password: SomeSecretPassword
128    schema: Schema1
129    site: Site1
130    template: Template1
131    anp: ANP1
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        anp=dict(type='str', required=True),
151        epg=dict(type='str', required=True),
152        pod=dict(type='str'),  # This parameter is not required for querying all objects
153        leaf=dict(type='str', aliases=['name']),
154        vlan=dict(type='int'),
155        state=dict(type='str', default='present', choices=['absent', 'present', 'query']),
156    )
157
158    module = AnsibleModule(
159        argument_spec=argument_spec,
160        supports_check_mode=True,
161        required_if=[
162            ['state', 'absent', ['pod', 'leaf', 'vlan']],
163            ['state', 'present', ['pod', 'leaf', 'vlan']],
164        ],
165    )
166
167    schema = module.params.get('schema')
168    site = module.params.get('site')
169    template = module.params.get('template').replace(' ', '')
170    anp = module.params.get('anp')
171    epg = module.params.get('epg')
172    pod = module.params.get('pod')
173    leaf = module.params.get('leaf')
174    vlan = module.params.get('vlan')
175    state = module.params.get('state')
176
177    leafpath = 'topology/{0}/node-{1}'.format(pod, leaf)
178
179    mso = MSOModule(module)
180
181    # Get schema objects
182    schema_id, schema_path, schema_obj = mso.query_schema(schema)
183
184    # Get site
185    site_id = mso.lookup_site(site)
186
187    # Get site_idx
188    if 'sites' not in schema_obj:
189        mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template))
190    sites = [(s.get('siteId'), s.get('templateName')) for s in schema_obj.get('sites')]
191    if (site_id, template) not in sites:
192        mso.fail_json(msg="Provided site/template '{0}-{1}' does not exist. Existing sites/templates: {2}".format(site, template, ', '.join(sites)))
193
194    # Schema-access uses indexes
195    site_idx = sites.index((site_id, template))
196    # Path-based access uses site_id-template
197    site_template = '{0}-{1}'.format(site_id, template)
198
199    # Get ANP
200    anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp)
201    anps = [a.get('anpRef') for a in schema_obj.get('sites')[site_idx]['anps']]
202    if anp_ref not in anps:
203        mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ', '.join(anps)))
204    anp_idx = anps.index(anp_ref)
205
206    # Get EPG
207    epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg)
208    epgs = [e.get('epgRef') for e in schema_obj.get('sites')[site_idx]['anps'][anp_idx]['epgs']]
209    if epg_ref not in epgs:
210        mso.fail_json(msg="Provided epg '{0}' does not exist. Existing epgs: {1}".format(epg, ', '.join(epgs)))
211    epg_idx = epgs.index(epg_ref)
212
213    # Get Leaf
214    leafs = [(leaf.get('path'), leaf.get('portEncapVlan')) for leaf in schema_obj.get('sites')[site_idx]['anps'][anp_idx]['epgs'][epg_idx]['staticLeafs']]
215    if (leafpath, vlan) in leafs:
216        leaf_idx = leafs.index((leafpath, vlan))
217        # FIXME: Changes based on index are DANGEROUS
218        leaf_path = '/sites/{0}/anps/{1}/epgs/{2}/staticLeafs/{3}'.format(site_template, anp, epg, leaf_idx)
219        mso.existing = schema_obj.get('sites')[site_idx]['anps'][anp_idx]['epgs'][epg_idx]['staticLeafs'][leaf_idx]
220
221    if state == 'query':
222        if leaf is None or vlan is None:
223            mso.existing = schema_obj.get('sites')[site_idx]['anps'][anp_idx]['epgs'][epg_idx]['staticLeafs']
224        elif not mso.existing:
225            mso.fail_json(msg="Static leaf '{leaf}/{vlan}' not found".format(leaf=leaf, vlan=vlan))
226        mso.exit_json()
227
228    leafs_path = '/sites/{0}/anps/{1}/epgs/{2}/staticLeafs'.format(site_template, anp, epg)
229    ops = []
230
231    mso.previous = mso.existing
232    if state == 'absent':
233        if mso.existing:
234            mso.sent = mso.existing = {}
235            ops.append(dict(op='remove', path=leaf_path))
236
237    elif state == 'present':
238        payload = dict(
239            path=leafpath,
240            portEncapVlan=vlan,
241        )
242
243        mso.sanitize(payload, collate=True)
244
245        if mso.existing:
246            ops.append(dict(op='replace', path=leaf_path, value=mso.sent))
247        else:
248            ops.append(dict(op='add', path=leafs_path + '/-', value=mso.sent))
249
250        mso.existing = mso.proposed
251
252    if not module.check_mode:
253        mso.request(schema_path, method='PATCH', data=ops)
254
255    mso.exit_json()
256
257
258if __name__ == "__main__":
259    main()
260