Skip to content

Commit 13cb4cd

Browse files
Merge pull request #157 from meraki/develop
v2.25.1
2 parents 55d1e68 + fc002b2 commit 13cb4cd

11 files changed

Lines changed: 307 additions & 81 deletions

File tree

‎.github/workflows/ansible-lint.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
steps:
1111
- uses: actions/checkout@v4
1212
- name: Run ansible-lint
13-
uses: ansible/ansible-lint@main
13+
uses: ansible/ansible-lint@v26
1414
with:
1515
args: ""
1616
setup_python: "true"

‎.github/workflows/certification.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,16 @@ concurrency:
3535
# for each affected version of ansible-core (for example, `tests/sanity/ignore-2.18.txt`) and add corresponding entries.
3636
jobs:
3737
call:
38-
uses: ansible-collections/partner-certification-checker/.github/workflows/certification-reusable.yml@v2.0.0 # zizmor: ignore[unpinned-uses]
38+
uses: ansible-collections/partner-certification-checker/.github/workflows/certification-reusable.yml@v4.1.0 # zizmor: ignore[unpinned-uses]
3939
# If the minimal Ansible Core version your collection supports Ansible
4040
# is greater than 2.16.0, specify it below.
4141
# You might also have to specify Python version supported by that version,
4242
# see https://docs.ansible.com/projects/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix
4343
# with:
4444
# ansible-core-version: '2.17.0'
45-
# python-versions: '["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]'
45+
# origin-python-version: '3.12'
4646
# collection-root: 'ansible_collections/junipernetworks/junos'
47+
# collection-deps: ''
4748
# automation-hub: true
4849
# Uncomment the lines below if you have 'automation-hub: true' and need to install collections from Red Hat Ansible Automation Hub.
4950
# This requires setting a repository secret called 'AH_TOKEN' which value contains a Red Hat Ansible Automation Hub token.

‎CHANGELOG.rst‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ Cisco.Meraki Release Notes
44

55
.. contents:: Topics
66

7+
v2.25.1
8+
=======
9+
10+
Bugfixes
11+
--------
12+
13+
- devices_management_interface - Removed the erroneous mapping of the create operation to rebootDevice; state=present now always updates the management
14+
interface, since it is a resource that always pre-exists and has no create operation.
15+
- devices_management_interface - Removed the bare exception handler in get_object_by_name() that swallowed API read failures (including the module's own
16+
fail_json) and misreported them as the object not existing.
17+
- plugin_utils/meraki - exec_meraki() now fails the task when a write operation's API response explicitly reports "success": false (as rebootDevice and
18+
similar fire-and-forget endpoints do on rejection), instead of letting the calling module report changed=true/failed=false. Responses without a "success"
19+
key, and read operations, are unaffected.
20+
721
v2.25.0
822
=======
923

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ This collection has been tested against the following environments:
160160
| 1.57.0 | 2.21.2 | 1.57.0 |
161161
| 1.68.0 | 2.23.0 | 2.2.0 |
162162
| 1.69.0 | 2.24.2 | 3.0.0 |
163-
| 1.72.0 | 2.25.0 | 4.4.0 |
163+
| 1.72.0 | 2.25.1 | 4.4.0 |
164164
165165
*Notes*:
166166

‎changelogs/changelog.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,3 +1640,14 @@ releases:
16401640
- "devices_wireless_bluetooth_settings - Fixed a malformed UUID in the module's EXAMPLES block."
16411641
- "networks_wireless_bluetooth_settings - Fixed a malformed UUID in the module's EXAMPLES block."
16421642
release_date: "2026-08-10"
1643+
2.25.1:
1644+
changes:
1645+
bugfixes:
1646+
- "devices_management_interface - Removed the erroneous mapping of the create operation to rebootDevice; state=present now always updates the management
1647+
interface, since it is a resource that always pre-exists and has no create operation."
1648+
- "devices_management_interface - Removed the bare exception handler in get_object_by_name() that swallowed API read failures (including the module's
1649+
own fail_json) and misreported them as the object not existing."
1650+
- "plugin_utils/meraki - exec_meraki() now fails the task when a write operation's API response explicitly reports \"success\": false (as rebootDevice
1651+
and similar fire-and-forget endpoints do on rejection), instead of letting the calling module report changed=true/failed=false. Responses without a
1652+
\"success\" key, and read operations, are unaffected."
1653+
release_date: "2026-08-18"

‎galaxy.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
namespace: cisco
33
name: meraki
4-
version: 2.25.0
4+
version: 2.25.1
55
readme: README.md
66
authors:
77
- Francisco Muñoz <fmunoz@cloverhound.com>

‎plugins/action/devices_management_interface.py‎

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,6 @@ def get_all_params(self, name=None, id=None):
6060
new_object_params['serial'] = self.new_object.get('serial')
6161
return new_object_params
6262

63-
def create_params(self):
64-
new_object_params = {}
65-
if self.new_object.get('serial') is not None or self.new_object.get(
66-
'serial') is not None:
67-
new_object_params['serial'] = self.new_object.get('serial') or \
68-
self.new_object.get('serial')
69-
return new_object_params
70-
7163
def update_all_params(self):
7264
new_object_params = {}
7365
if self.new_object.get('wan1') is not None or self.new_object.get(
@@ -87,21 +79,20 @@ def update_all_params(self):
8779
def get_object_by_name(self, name):
8880
result = None
8981
# NOTE: Does not have a get by name method, using get all
90-
try:
91-
items = self.meraki.exec_meraki(
92-
family="devices",
93-
function="getDeviceManagementInterface",
94-
params=self.get_all_params(name=name),
95-
)
96-
if isinstance(items, dict):
97-
if 'response' in items:
98-
items = items.get('response')
99-
result = get_dict_result(items, 'name', name)
100-
if result is None:
101-
result = items
102-
except Exception as e:
103-
print("Error: ", e)
104-
result = None
82+
# exec_meraki() already raises AnsibleActionFail on an API error, so a
83+
# failed read (bad serial, rate limit, etc.) fails the task instead of
84+
# being mistaken for "the object does not exist".
85+
items = self.meraki.exec_meraki(
86+
family="devices",
87+
function="getDeviceManagementInterface",
88+
params=self.get_all_params(name=name),
89+
)
90+
if isinstance(items, dict):
91+
if 'response' in items:
92+
items = items.get('response')
93+
result = get_dict_result(items, 'name', name)
94+
if result is None:
95+
result = items
10596
return result
10697

10798
def get_object_by_id(self, id):
@@ -147,15 +138,6 @@ def requires_update(self, current_obj):
147138
meraki_param,
148139
ansible_param) in obj_params)
149140

150-
def create(self):
151-
result = self.meraki.exec_meraki(
152-
family="devices",
153-
function="rebootDevice",
154-
params=self.create_params(),
155-
op_modifies=True,
156-
)
157-
return result
158-
159141
def update(self):
160142
id = self.new_object.get("id")
161143
name = self.new_object.get("name")
@@ -210,17 +192,22 @@ def run(self, tmp=None, task_vars=None):
210192

211193
response = None
212194
if state == "present":
195+
# A device's management interface always exists; there is no
196+
# create operation for it (the only write is a PUT). Reaching
197+
# this point with obj_exists False means the read returned an
198+
# unexpected shape rather than failing outright, since a real
199+
# failure already raised inside exists() -> get_object_by_name().
213200
(obj_exists, prev_obj) = obj.exists()
214-
if obj_exists:
215-
if obj.requires_update(prev_obj):
216-
response = obj.update()
217-
meraki.object_updated()
218-
else:
219-
response = prev_obj
220-
meraki.object_already_present()
201+
if not obj_exists:
202+
raise AnsibleActionFail(
203+
"Unable to retrieve the current management interface for serial '{serial}'.".format(
204+
serial=self._task.args.get("serial")))
205+
if obj.requires_update(prev_obj):
206+
response = obj.update()
207+
meraki.object_updated()
221208
else:
222-
response = obj.create()
223-
meraki.object_created()
209+
response = prev_obj
210+
meraki.object_already_present()
224211

225212
self._result.update(dict(meraki_response=response))
226213
self._result.update(meraki.exit_json())

‎plugins/modules/devices_management_interface.py‎

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
module: devices_management_interface
1010
short_description: Resource module for devices _management _interface
1111
description:
12-
- Manage operations create and update of the resource devices _management _interface.
13-
- Reboot a device. This endpoint has a sustained rate limit of one request every
14-
60 seconds.
12+
- Manage operations update of the resource devices _management _interface.
1513
- Update the management interface settings for a device.
14+
- A device's management interface always exists, so C(state=present) always
15+
updates it; there is no create operation for this resource.
1616
version_added: '1.0.0'
1717
extends_documentation_fragment:
1818
- cisco.meraki.module
@@ -81,18 +81,13 @@
8181
- meraki >= 2.4.9
8282
- python >= 3.5
8383
seealso:
84-
- name: Cisco Meraki documentation for devices rebootDevice
85-
description: Complete reference of the rebootDevice API.
86-
link: https://developer.cisco.com/meraki/api-v1/#!reboot-device
8784
- name: Cisco Meraki documentation for devices updateDeviceManagementInterface
8885
description: Complete reference of the updateDeviceManagementInterface API.
8986
link: https://developer.cisco.com/meraki/api-v1/#!update-device-management-interface
9087
notes:
91-
- SDK Method used are
92-
devices.Devices.reboot_device,
88+
- SDK Method used is
9389
devices.Devices.update_device_management_interface,
9490
- Paths used are
95-
post /devices/{serial}/reboot,
9691
put /devices/{serial}/managementInterface,
9792
"""
9893

@@ -142,31 +137,6 @@
142137
usingStaticIp: false
143138
vlan: 2
144139
wanEnabled: enabled
145-
- name: Create
146-
cisco.meraki.devices_management_interface:
147-
meraki_api_key: "{{ meraki_api_key }}"
148-
meraki_base_url: "{{ meraki_base_url }}"
149-
meraki_single_request_timeout: "{{ meraki_single_request_timeout }}"
150-
meraki_certificate_path: "{{ meraki_certificate_path }}"
151-
meraki_requests_proxy: "{{ meraki_requests_proxy }}"
152-
meraki_wait_on_rate_limit: "{{ meraki_wait_on_rate_limit }}"
153-
meraki_nginx_429_retry_wait_time: "{{ meraki_nginx_429_retry_wait_time }}"
154-
meraki_action_batch_retry_wait_time: "{{ meraki_action_batch_retry_wait_time }}"
155-
meraki_retry_4xx_error: "{{ meraki_retry_4xx_error }}"
156-
meraki_retry_4xx_error_wait_time: "{{ meraki_retry_4xx_error_wait_time }}"
157-
meraki_maximum_retries: "{{ meraki_maximum_retries }}"
158-
meraki_output_log: "{{ meraki_output_log }}"
159-
meraki_log_file_prefix: "{{ meraki_log_file_prefix }}"
160-
meraki_log_path: "{{ meraki_log_path }}"
161-
meraki_print_console: "{{ meraki_print_console }}"
162-
meraki_suppress_logging: "{{ meraki_suppress_logging }}"
163-
meraki_simulate: "{{ meraki_simulate }}"
164-
meraki_be_geo_id: "{{ meraki_be_geo_id }}"
165-
meraki_caller: "{{ meraki_caller }}"
166-
meraki_use_iterator_for_get_pages: "{{ meraki_use_iterator_for_get_pages }}"
167-
meraki_inherit_logging_config: "{{ meraki_inherit_logging_config }}"
168-
state: present
169-
serial: string
170140
"""
171141
RETURN = r"""
172142
meraki_response:

‎plugins/plugin_utils/meraki.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def __init__(self, params):
285285
suppress_logging=params.get("meraki_suppress_logging"),
286286
simulate=params.get("meraki_simulate"),
287287
be_geo_id=params.get("meraki_be_geo_id"),
288-
caller="MerakiAnsibleCollection/2.25.0 Cisco",
288+
caller="MerakiAnsibleCollection/2.25.1 Cisco",
289289
use_iterator_for_get_pages=params.get(
290290
"meraki_use_iterator_for_get_pages"),
291291
inherit_logging_config=params.get(
@@ -334,6 +334,7 @@ def extract_file_name(self, file_path):
334334
return os.path.basename(file_path)
335335

336336
def exec_meraki(self, family, function, params=None, op_modifies=False, **kwargs):
337+
family_name = family
337338
try:
338339
family = getattr(self.api, family)
339340
func = getattr(family, function)
@@ -369,6 +370,22 @@ def exec_meraki(self, family, function, params=None, op_modifies=False, **kwargs
369370
"The error was: {error}"
370371
).format(error=to_native(e))
371372
)
373+
# Some write operations (reboot, cycle ports, blink LEDs, unenroll,
374+
# batch updates, ...) return 2xx with a body of the form
375+
# {"success": false} instead of raising an APIError when the
376+
# requested action is rejected. Without this check that response
377+
# still looks like a normal successful result to the calling action
378+
# plugin, which reports changed=True/failed=False even though
379+
# nothing happened. A response with no "success" key (e.g. the
380+
# updated object itself, as returned by most update* operations) is
381+
# unaffected: only an explicit `False` is treated as a failure.
382+
if op_modifies and isinstance(response, dict) and response.get("success") is False:
383+
self.fail_json(
384+
msg=(
385+
"{family}, {function} - the operation was not successful "
386+
"(the API response reported success: false). Response: {response}"
387+
).format(family=family_name, function=function, response=response)
388+
)
372389
return response
373390

374391
def fail_json(self, msg, **kwargs):

0 commit comments

Comments
 (0)