Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

#!/usr/bin/python 

# -*- coding: utf-8 -*- 

 

# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net> 

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 

 

from __future__ import absolute_import, division, print_function 

__metaclass__ = type 

 

ANSIBLE_METADATA = { 

'metadata_version': '1.1', 

'status': ['preview'], 

'supported_by': 'community' 

} 

 

DOCUMENTATION = r''' 

--- 

module: meraki_device 

short_description: Manage devices in the Meraki cloud 

version_added: "2.7" 

description: 

- Visibility into devices associated to a Meraki environment. 

notes: 

- This module does not support claiming of devices or licenses into a Meraki organization. 

- More information about the Meraki API can be found at U(https://dashboard.meraki.com/api_docs). 

- Some of the options are likely only used for developers within Meraki. 

options: 

state: 

description: 

- Query an organization. 

choices: [absent, present, query] 

default: query 

type: str 

net_name: 

description: 

- Name of a network. 

aliases: [network] 

type: str 

net_id: 

description: 

- ID of a network. 

type: str 

serial: 

description: 

- Serial number of a device to query. 

type: str 

hostname: 

description: 

- Hostname of network device to search for. 

aliases: [name] 

type: str 

model: 

description: 

- Model of network device to search for. 

type: str 

tags: 

description: 

- Space delimited list of tags to assign to device. 

type: str 

lat: 

description: 

- Latitude of device's geographic location. 

- Use negative number for southern hemisphere. 

aliases: [latitude] 

type: float 

lng: 

description: 

- Longitude of device's geographic location. 

- Use negative number for western hemisphere. 

aliases: [longitude] 

type: float 

address: 

description: 

- Postal address of device's location. 

type: str 

move_map_marker: 

description: 

- Whether or not to set the latitude and longitude of a device based on the new address. 

- Only applies when C(lat) and C(lng) are not specified. 

type: bool 

serial_lldp_cdp: 

description: 

- Serial number of device to query LLDP/CDP information from. 

type: str 

lldp_cdp_timespan: 

description: 

- Timespan, in seconds, used to query LLDP and CDP information. 

- Must be less than 1 month. 

type: int 

serial_uplink: 

description: 

- Serial number of device to query uplink information from. 

type: str 

note: 

description: 

- Informational notes about a device. 

- Limited to 255 characters. 

version_added: '2.8' 

type: str 

 

 

author: 

- Kevin Breit (@kbreit) 

extends_documentation_fragment: meraki 

''' 

 

EXAMPLES = r''' 

- name: Query all devices in an organization. 

meraki_device: 

auth_key: abc12345 

org_name: YourOrg 

state: query 

delegate_to: localhost 

 

- name: Query all devices in a network. 

meraki_device: 

auth_key: abc12345 

org_name: YourOrg 

net_name: YourNet 

state: query 

delegate_to: localhost 

 

- name: Query a device by serial number. 

meraki_device: 

auth_key: abc12345 

org_name: YourOrg 

net_name: YourNet 

serial: ABC-123 

state: query 

delegate_to: localhost 

 

- name: Lookup uplink information about a device. 

meraki_device: 

auth_key: abc12345 

org_name: YourOrg 

net_name: YourNet 

serial_uplink: ABC-123 

state: query 

delegate_to: localhost 

 

- name: Lookup LLDP and CDP information about devices connected to specified device. 

meraki_device: 

auth_key: abc12345 

org_name: YourOrg 

net_name: YourNet 

serial_lldp_cdp: ABC-123 

state: query 

delegate_to: localhost 

 

- name: Lookup a device by hostname. 

meraki_device: 

auth_key: abc12345 

org_name: YourOrg 

net_name: YourNet 

hostname: main-switch 

state: query 

delegate_to: localhost 

 

- name: Query all devices of a specific model. 

meraki_device: 

auth_key: abc123 

org_name: YourOrg 

net_name: YourNet 

model: MR26 

state: query 

delegate_to: localhost 

 

- name: Update information about a device. 

meraki_device: 

auth_key: abc123 

org_name: YourOrg 

net_name: YourNet 

state: present 

serial: '{{serial}}' 

name: mr26 

address: 1060 W. Addison St., Chicago, IL 

lat: 41.948038 

lng: -87.65568 

tags: recently-added 

delegate_to: localhost 

 

- name: Claim a device into a network. 

meraki_device: 

auth_key: abc123 

org_name: YourOrg 

net_name: YourNet 

serial: ABC-123 

state: present 

delegate_to: localhost 

 

- name: Remove a device from a network. 

meraki_device: 

auth_key: abc123 

org_name: YourOrg 

net_name: YourNet 

serial: ABC-123 

state: absent 

delegate_to: localhost 

''' 

 

RETURN = r''' 

response: 

description: Data returned from Meraki dashboard. 

type: dict 

returned: info 

''' 

 

from ansible.module_utils.basic import AnsibleModule, json 

from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec 

 

 

def format_tags(tags): 

""" Add a space before and after the list of tags """ 

return " {tags} ".format(tags=tags) 

 

 

def is_device_valid(meraki, serial, data): 

""" Parse a list of devices for a serial and return True if it's in the list """ 

for device in data: 

220 ↛ 219line 220 didn't jump to line 219, because the condition on line 220 was never false if device['serial'] == serial: 

return True 

return False 

 

 

def get_org_devices(meraki, org_id): 

""" Get all devices in an organization """ 

path = meraki.construct_path('get_all_org', org_id=org_id) 

response = meraki.request(path, method='GET') 

if meraki.status != 200: 

meraki.fail_json(msg='Failed to query all devices belonging to the organization') 

return response 

 

 

def get_net_devices(meraki, net_id): 

""" Get all devices in a network """ 

path = meraki.construct_path('get_all', net_id=net_id) 

response = meraki.request(path, method='GET') 

238 ↛ 239line 238 didn't jump to line 239, because the condition on line 238 was never true if meraki.status != 200: 

meraki.fail_json(msg='Failed to query all devices belonging to the network') 

return response 

 

def construct_payload(params): 

""" Create payload based on inputs """ 

payload = {} 

245 ↛ 247line 245 didn't jump to line 247, because the condition on line 245 was never false if params['hostname'] is not None: 

payload['name'] = params['hostname'] 

247 ↛ 249line 247 didn't jump to line 249, because the condition on line 247 was never false if params['tags'] is not None: 

payload['tags'] = format_tags(params['tags']) 

249 ↛ 251line 249 didn't jump to line 251, because the condition on line 249 was never false if params['lat'] is not None: 

payload['lat'] = params['lat'] 

251 ↛ 253line 251 didn't jump to line 253, because the condition on line 251 was never false if params['lng'] is not None: 

payload['lng'] = params['lng'] 

253 ↛ 255line 253 didn't jump to line 255, because the condition on line 253 was never false if params['address'] is not None: 

payload['address'] = params['address'] 

255 ↛ 257line 255 didn't jump to line 257, because the condition on line 255 was never false if params['move_map_marker'] is not None: 

payload['moveMapMarker'] = params['move_map_marker'] 

257 ↛ 259line 257 didn't jump to line 259, because the condition on line 257 was never false if params['note'] is not None: 

payload['notes'] = params['note'] 

return payload 

 

 

def main(): 

 

# define the available arguments/parameters that a user can pass to 

# the module 

argument_spec = meraki_argument_spec() 

argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='query'), 

net_name=dict(type='str', aliases=['network']), 

net_id=dict(type='str'), 

serial=dict(type='str'), 

serial_uplink=dict(type='str'), 

serial_lldp_cdp=dict(type='str'), 

lldp_cdp_timespan=dict(type='int'), 

hostname=dict(type='str', aliases=['name']), 

model=dict(type='str'), 

tags=dict(type='str', default=None), 

lat=dict(type='float', aliases=['latitude'], default=None), 

lng=dict(type='float', aliases=['longitude'], default=None), 

address=dict(type='str', default=None), 

move_map_marker=dict(type='bool', default=None), 

note=dict(type='str', default=None), 

) 

 

# the AnsibleModule object will be our abstraction working with Ansible 

# this includes instantiation, a couple of common attr would be the 

# args/params passed to the execution, as well as if the module 

# supports check mode 

module = AnsibleModule(argument_spec=argument_spec, 

supports_check_mode=False, 

) 

meraki = MerakiModule(module, function='device') 

 

293 ↛ 294line 293 didn't jump to line 294, because the condition on line 293 was never true if meraki.params['serial_lldp_cdp'] and not meraki.params['lldp_cdp_timespan']: 

meraki.fail_json(msg='lldp_cdp_timespan is required when querying LLDP and CDP information') 

295 ↛ 296line 295 didn't jump to line 296, because the condition on line 295 was never true if meraki.params['net_name'] and meraki.params['net_id']: 

meraki.fail_json(msg='net_name and net_id are mutually exclusive') 

 

meraki.params['follow_redirects'] = 'all' 

 

query_urls = {'device': '/networks/{net_id}/devices'} 

query_org_urls = {'device': '/organizations/{org_id}/inventory'} 

query_device_urls = {'device': '/networks/{net_id}/devices/'} 

claim_device_urls = {'device': '/networks/{net_id}/devices/claim'} 

bind_org_urls = {'device': '/organizations/{org_id}/claim'} 

update_device_urls = {'device': '/networks/{net_id}/devices/'} 

delete_device_urls = {'device': '/networks/{net_id}/devices/'} 

 

meraki.url_catalog['get_all'].update(query_urls) 

meraki.url_catalog['get_all_org'] = query_org_urls 

meraki.url_catalog['get_device'] = query_device_urls 

meraki.url_catalog['create'] = claim_device_urls 

meraki.url_catalog['bind_org'] = bind_org_urls 

meraki.url_catalog['update'] = update_device_urls 

meraki.url_catalog['delete'] = delete_device_urls 

 

payload = None 

 

# if the user is working with this module in only check mode we do not 

# want to make any changes to the environment, just return the current 

# state with no modifications 

# FIXME: Work with Meraki so they can implement a check mode 

322 ↛ 323line 322 didn't jump to line 323, because the condition on line 322 was never true if module.check_mode: 

meraki.exit_json(**meraki.result) 

 

# execute checks for argument completeness 

 

# manipulate or modify the state as needed (this is going to be the 

# part where your module will do what it needs to do) 

org_id = meraki.params['org_id'] 

330 ↛ 332line 330 didn't jump to line 332, because the condition on line 330 was never false if org_id is None: 

org_id = meraki.get_org_id(meraki.params['org_name']) 

nets = meraki.get_nets(org_id=org_id) 

net_id = None 

if meraki.params['net_id'] or meraki.params['net_name']: 

net_id = meraki.params['net_id'] 

if net_id is None: 

net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets) 

 

if meraki.params['state'] == 'query': 

if meraki.params['net_name'] or meraki.params['net_id']: 

device = [] 

if meraki.params['serial']: 

path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial'] 

request = meraki.request(path, method='GET') 

device.append(request) 

meraki.result['data'] = device 

elif meraki.params['serial_uplink']: 

path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial_uplink'] + '/uplink' 

meraki.result['data'] = (meraki.request(path, method='GET')) 

elif meraki.params['serial_lldp_cdp']: 

351 ↛ 352line 351 didn't jump to line 352, because the condition on line 351 was never true if meraki.params['lldp_cdp_timespan'] > 2592000: 

meraki.fail_json(msg='LLDP/CDP timespan must be less than a month (2592000 seconds)') 

path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial_lldp_cdp'] + '/lldp_cdp' 

path = path + '?timespan=' + str(meraki.params['lldp_cdp_timespan']) 

device.append(meraki.request(path, method='GET')) 

meraki.result['data'] = device 

elif meraki.params['hostname']: 

path = meraki.construct_path('get_all', net_id=net_id) 

devices = meraki.request(path, method='GET') 

for unit in devices: 

try: 

362 ↛ 363line 362 didn't jump to line 363, because the condition on line 362 was never true if unit['name'] == meraki.params['hostname']: 

device.append(unit) 

meraki.result['data'] = device 

except KeyError: 

pass 

elif meraki.params['model']: 

path = meraki.construct_path('get_all', net_id=net_id) 

devices = meraki.request(path, method='GET') 

device_match = [] 

for device in devices: 

372 ↛ 373line 372 didn't jump to line 373, because the condition on line 372 was never true if device['model'] == meraki.params['model']: 

device_match.append(device) 

meraki.result['data'] = device_match 

else: 

path = meraki.construct_path('get_all', net_id=net_id) 

request = meraki.request(path, method='GET') 

meraki.result['data'] = request 

else: 

path = meraki.construct_path('get_all_org', org_id=org_id) 

devices = meraki.request(path, method='GET') 

if meraki.params['serial']: 

for device in devices: 

if device['serial'] == meraki.params['serial']: 

meraki.result['data'] = device 

else: 

meraki.result['data'] = devices 

elif meraki.params['state'] == 'present': 

device = [] 

390 ↛ 391line 390 didn't jump to line 391, because the condition on line 390 was never true if net_id is None: # Claim a device to an organization 

device_list = get_org_devices(meraki, org_id) 

if is_device_valid(meraki, meraki.params['serial'], device_list) is False: 

payload = {'serial': meraki.params['serial']} 

path = meraki.construct_path('bind_org', org_id=org_id) 

created_device = [] 

created_device.append(meraki.request(path, method='POST', payload=json.dumps(payload))) 

meraki.result['data'] = created_device 

meraki.result['changed'] = True 

else: # A device is assumed to be in an organization 

device_list = get_net_devices(meraki, net_id) 

if is_device_valid(meraki, meraki.params['serial'], device_list) is True: # Device is in network, update 

query_path = meraki.construct_path('get_all', net_id=net_id) 

403 ↛ 439line 403 didn't jump to line 439, because the condition on line 403 was never false if is_device_valid(meraki, meraki.params['serial'], device_list): 

payload = construct_payload(meraki.params) 

query_path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial'] 

device_data = meraki.request(query_path, method='GET') 

ignore_keys = ['lanIp', 'serial', 'mac', 'model', 'networkId', 'moveMapMarker', 'wan1Ip', 'wan2Ip'] 

if meraki.is_update_required(device_data, payload, optional_ignore=ignore_keys): 

path = meraki.construct_path('update', net_id=net_id) + meraki.params['serial'] 

updated_device = [] 

updated_device.append(meraki.request(path, method='PUT', payload=json.dumps(payload))) 

meraki.result['data'] = updated_device 

meraki.result['changed'] = True 

else: 

meraki.result['data'] = device_data 

else: # Device needs to be added to network 

query_path = meraki.construct_path('get_all', net_id=net_id) 

device_list = meraki.request(query_path, method='GET') 

419 ↛ 439line 419 didn't jump to line 439, because the condition on line 419 was never false if is_device_valid(meraki, meraki.params['serial'], device_list) is False: 

420 ↛ 439line 420 didn't jump to line 439, because the condition on line 420 was never false if net_id: 

payload = {'serial': meraki.params['serial']} 

path = meraki.construct_path('create', net_id=net_id) 

created_device = [] 

created_device.append(meraki.request(path, method='POST', payload=json.dumps(payload))) 

meraki.result['data'] = created_device 

meraki.result['changed'] = True 

427 ↛ 439line 427 didn't jump to line 439, because the condition on line 427 was never false elif meraki.params['state'] == 'absent': 

device = [] 

query_path = meraki.construct_path('get_all', net_id=net_id) 

device_list = meraki.request(query_path, method='GET') 

431 ↛ 439line 431 didn't jump to line 439, because the condition on line 431 was never false if is_device_valid(meraki, meraki.params['serial'], device_list) is True: 

path = meraki.construct_path('delete', net_id=net_id) 

path = path + meraki.params['serial'] + '/remove' 

request = meraki.request(path, method='POST') 

meraki.result['changed'] = True 

 

# in the event of a successful module execution, you will want to 

# simple AnsibleModule.exit_json(), passing the key/value results 

meraki.exit_json(**meraki.result) 

 

 

442 ↛ exitline 442 didn't exit the module, because the condition on line 442 was never falseif __name__ == '__main__': 

main()