Coverage for /Users/jingyu.wy/git/ansible-module-apsarastack/src/ansible_collections/alibaba/apsarastack/plugins/modules/ali_security_group.py: 34%

147 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 18:03 +0800

1#!/usr/bin/python 

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

3 

4# Copyright (c) 2017-present Alibaba Group Holding Limited. He Guimin <heguimin36@163.com.com> 

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

6# 

7# This file is part of Ansible 

8# 

9# Ansible is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# Ansible is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with Ansible. If not, see http://www.gnu.org/licenses/. 

21 

22from __future__ import (absolute_import, division, print_function) 

23 

24__metaclass__ = type 

25 

26ANSIBLE_METADATA = {'metadata_version': '1.1', 

27 'status': ['preview'], 

28 'supported_by': 'community'} 

29 

30DOCUMENTATION = ''' 

31--- 

32module: ali_security_group 

33short_description: Manage Alibaba Cloud Security Group and its rules. 

34description: 

35 - Create and Delete Security Group, Modify its description and add/remove rules. 

36options: 

37 state: 

38 description: 

39 - Create, delete a security group 

40 default: 'present' 

41 choices: ['present', 'absent'] 

42 type: str 

43 name: 

44 description: 

45 - Name of the security group, which is a string of 2 to 128 Chinese or English characters. It must begin with an 

46 uppercase/lowercase letter or a Chinese character and can contain numerals, "_", "." or "-". 

47 It cannot begin with http:// or https://. 

48 This is used in combination with C(vpc_id) to determine if a Securty Group already exists. 

49 required: True 

50 aliases: ['group_name'] 

51 type: str 

52 description: 

53 description: 

54 - Description of the security group, which is a string of 2 to 256 characters. 

55 - It cannot begin with http:// or https://. 

56 type: str 

57 vpc_id: 

58 description: 

59 - ID of the VPC to create the group in. This is used in conjunction with the C(name) to ensure idempotence. 

60 type: str 

61 rules: 

62 description: 

63 - List of hash/dictionaries firewall inbound rules to enforce in this group (see example). If none are supplied, 

64 no inbound rules will be enabled. Each rule has several keys and refer to 

65 https://www.alibabacloud.com/help/doc-detail/25554.htm. Each key should be format as under_score. 

66 At present, the valid keys including "ip_protocol", "port_range", "source_port_range", "nic_type", "policy", 

67 "dest_cidr_ip", "source_cidr_ip", "source_group_id", "source_group_owner_account", "source_group_owner_id", 

68 "priority" and "description". 

69 type: list 

70 elements: dict 

71 rules_egress: 

72 description: 

73 - List of hash/dictionaries firewall outbound rules to enforce in this group (see example). If none are supplied, 

74 no outbound rules will be enabled. Each rule has several keys and refer to 

75 https://www.alibabacloud.com/help/doc-detail/25560.htm. Each key should be format as under_score. 

76 At present, the valid keys including "ip_protocol", "port_range", "source_port_range", "nic_type", "policy", 

77 "dest_cidr_ip", "source_cidr_ip", "dest_group_id", "dest_group_owner_account", "dest_group_owner_id", 

78 "priority" and "description". 

79 type: list 

80 elements: dict 

81 purge_rules: 

82 description: 

83 - Purge existing rules on security group that are not found in rules 

84 default: True 

85 type: bool 

86 purge_rules_egress: 

87 description: 

88 - Purge existing rules_egress on security group that are not found in rules_egress 

89 default: True 

90 type: bool 

91 security_group_id: 

92 description: 

93 - Security group ID. 

94 aliases: ['id'] 

95 type: str 

96 tags: 

97 description: 

98 - A hash/dictionaries of security group tags. C({"key":"value"}) 

99 aliases: ["group_tags"] 

100 type: dict 

101 multi_ok: 

102 description: 

103 - By default the module will not create another Security Group if there is another Security Group 

104 with the same I(name) or I(vpc_id). Specify this as true if you want duplicate Security Groups created. 

105 There will be conflict when I(multi_ok=True) and I(recent=True). 

106 default: False 

107 type: bool 

108 recent: 

109 description: 

110 - By default the module will not choose the recent one if there is another Security Group with 

111 the same I(name) or I(vpc_id). Specify this as true if you want to target the recent Security Group. 

112 There will be conflict when I(multi_ok=True) and I(recent=True). 

113 default: False 

114 type: bool 

115requirements: 

116 - "python >= 3.6" 

117 - "footmark >= 1.13.0" 

118extends_documentation_fragment: 

119 - apsarastack 

120author: 

121 - "He Guimin (@xiaozhu36)" 

122''' 

123 

124EXAMPLES = ''' 

125# Note: These examples do not set authentication details, see the Alibaba Cloud Guide for details. 

126- name: Create a new security group 

127 ali_security_group: 

128 name: 'AliyunSG' 

129 vpc_id: 'vpc-123csecd' 

130 

131- name: Authorize security group 

132 ali_security_group: 

133 name: 'AliyunSG' 

134 vpc_id: 'vpc-123csecd' 

135 rules: 

136 - ip_protocol: tcp 

137 port_range: 1/122 

138 source_cidr_ip: '10.159.6.18/12' 

139 rules_egress: 

140 - ip_protocol: icmp 

141 port_range: -1/-1 

142 source_cidr_ip: 10.0.0.0/10 

143 dest_group_id: 'sg-ce33rdsfe' 

144 priority: 1 

145 

146- name: Delete security grp 

147 ali_security_group: 

148 name: 'AliyunSG' 

149 vpc_id: 'vpc-123csecd' 

150 state: absent 

151''' 

152 

153RETURN = ''' 

154group: 

155 description: Dictionary of security group values 

156 returned: always 

157 type: complex 

158 contains: 

159 description: 

160 description: The Security Group description. 

161 returned: always 

162 type: str 

163 sample: "my ansible group" 

164 group_name: 

165 description: Security group name 

166 sample: "my-ansible-group" 

167 type: str 

168 returned: always 

169 group_id: 

170 description: Security group id 

171 sample: sg-abcd1234 

172 type: str 

173 returned: always 

174 id: 

175 description: Alias of "group_id". 

176 sample: sg-abcd1234 

177 type: str 

178 returned: always 

179 inner_access_policy: 

180 description: Whether can access each other in one security group. 

181 sample: True 

182 type: bool 

183 returned: always 

184 tags: 

185 description: Tags associated with the security group 

186 type: dict 

187 sample: 

188 - Name: My Security Group 

189 From: Ansible 

190 type: dict 

191 returned: always 

192 returned: always 

193 vpc_id: 

194 description: ID of VPC to which the security group belongs 

195 sample: vpc-abcd1234 

196 type: str 

197 returned: always 

198 permissions: 

199 description: Inbound rules associated with the security group. 

200 sample: 

201 - create_time: "2018-06-28T08:45:58Z" 

202 description: "None" 

203 dest_cidr_ip: "None" 

204 dest_group_id: "None" 

205 dest_group_name: "None" 

206 dest_group_owner_account: "None" 

207 direction: "ingress" 

208 ip_protocol: "TCP" 

209 nic_type: "intranet" 

210 policy: "Accept" 

211 port_range: "22/22" 

212 priority: 1 

213 source_cidr_ip: "0.0.0.0/0" 

214 source_group_id: "None" 

215 source_group_name: "None" 

216 source_group_owner_account: "None" 

217 type: list 

218 returned: always 

219 permissions_egress: 

220 description: Outbound rules associated with the security group. 

221 sample: 

222 - create_time: "2018-06-28T08:45:59Z" 

223 description: "NOne" 

224 dest_cidr_ip: "192.168.0.54/32" 

225 dest_group_id: "None" 

226 dest_group_name: "None" 

227 dest_group_owner_account: "None" 

228 direction: "egress" 

229 ip_protocol: "TCP" 

230 nic_type: "intranet" 

231 policy: "Accept" 

232 port_range: "80/80" 

233 priority: 1 

234 source_cidr_ip: "None" 

235 source_group_id: "None" 

236 source_group_name: "None" 

237 source_group_owner_account: "None" 

238 type: list 

239 returned: always 

240''' 

241 

242import time 

243from ansible.module_utils.basic import AnsibleModule 

244from ansible_collections.alibaba.apsarastack.plugins.module_utils.apsarastack_common import common_argument_spec 

245from ansible_collections.alibaba.apsarastack.plugins.module_utils.apsarastack_connections import ecs_connect 

246 

247 

248try: 

249 from footmark.exception import ECSResponseError 

250 

251 HAS_FOOTMARK = True 

252except ImportError: 

253 HAS_FOOTMARK = False 

254 

255 

256VALID_INGRESS_PARAMS = ["ip_protocol", "port_range", "source_port_range", "nic_type", "policy", 

257 "dest_cidr_ip", "source_cidr_ip", "priority", "description", 

258 "source_group_id", "source_group_owner_account", "source_group_owner_id"] 

259VALID_EGRESS_PARAMS = ["ip_protocol", "port_range", "source_port_range", "nic_type", "policy", 

260 "dest_cidr_ip", "source_cidr_ip", "priority", "description", 

261 "dest_group_id", "dest_group_owner_account", "dest_group_owner_id"] 

262 

263 

264def validate_group_rule_keys(module, rule, direction): 

265 

266 if not isinstance(rule, dict): 

267 module.fail_json(msg='Invalid rule parameter type [{0}].'.format(type(rule))) 

268 

269 VALID_PARAMS = VALID_INGRESS_PARAMS 

270 if direction == "egress": 

271 VALID_PARAMS = VALID_EGRESS_PARAMS 

272 

273 for k in rule: 

274 if k not in VALID_PARAMS: 

275 module.fail_json(msg="Invalid rule parameter '{0}' for rule: {1}. Supported parametes include: {2}".format(k, rule, VALID_PARAMS)) 

276 

277 if 'ip_protocol' not in rule: 

278 module.fail_json(msg='ip_protocol is required.') 

279 if 'port_range' not in rule: 

280 module.fail_json(msg='port_range is required.') 

281 # The system response will return upper protocol 

282 rule['ip_protocol'] = str(rule['ip_protocol']).upper() 

283 

284 

285def purge_rules(module, group, existing_rule, rules, direction): 

286 

287 if not isinstance(existing_rule, dict): 

288 module.fail_json(msg='Invalid existing rule type [{0}].'.format(type(existing_rule))) 

289 

290 if not isinstance(rules, list): 

291 module.fail_json(msg='Invalid rules type [{0}]. The specified rules should be a list.'.format(type(rules))) 

292 

293 VALID_PARAMS = VALID_INGRESS_PARAMS 

294 if direction == "egress": 

295 VALID_PARAMS = VALID_EGRESS_PARAMS 

296 

297 # Find the rules which is not in the specified rules 

298 find = False 

299 for rule in rules: 

300 for key in VALID_PARAMS: 

301 if not rule.get(key): 

302 continue 

303 if existing_rule.get(key) != rule.get(key): 

304 find = False 

305 break 

306 find = True 

307 if find: 

308 break 

309 # If it is not found, there will not purge anythind 

310 if not find: 

311 return group.revoke(existing_rule, direction) 

312 return False 

313 

314 

315def group_exists(conn, module, vpc_id, name, security_group_id, multi, recent): 

316 """Returns None or a security group object depending on the existence of a security group. 

317 When supplied with a vpc_id and Name, it will check them to determine if it is a match 

318 otherwise it will assume the Security Group does not exist and thus return None. 

319 """ 

320 if multi: 

321 return None 

322 matching_groups = [] 

323 filters = {} 

324 if vpc_id: 

325 filters['vpc_id'] = vpc_id 

326 if security_group_id: 

327 filters['security_group_id'] = security_group_id 

328 try: 

329 for g in conn.describe_security_groups(**filters): 

330 if name and g.security_group_name != name: 

331 continue 

332 matching_groups.append(g.get()) 

333 except Exception as e: 

334 module.fail_json(msg="Failed to describe Security Groups: {0}".format(e)) 

335 

336 if len(matching_groups) == 1: 

337 return matching_groups[0] 

338 elif len(matching_groups) > 1: 

339 if recent: 

340 return matching_groups[-1] 

341 module.fail_json(msg='Currently there are {0} Security Groups that have the same name and ' 

342 'vpc id you specified. If you would like to create anyway ' 

343 'please pass True to the multi_ok param. Or, please pass True to the recent ' 

344 'param to choose the recent one.'.format(len(matching_groups))) 

345 return None 

346 

347 

348def main(): 

349 argument_spec = common_argument_spec() 

350 argument_spec.update(dict( 

351 state=dict(default='present', type='str', choices=['present', 'absent']), 

352 name=dict(type='str', required=True, aliases=['group_name']), 

353 description=dict(type='str'), 

354 vpc_id=dict(type='str'), 

355 security_group_id=dict(type='str', aliases=['id', 'group_id']), 

356 tags=dict(type='dict', aliases=['group_tags']), 

357 rules=dict(type='list', elements='dict'), 

358 rules_egress=dict(type='list', elements='dict'), 

359 purge_rules=dict(type='bool', default=True), 

360 purge_rules_egress=dict(type='bool', default=True), 

361 multi_ok=dict(type='bool', default=False), 

362 recent=dict(type='bool', default=False) 

363 )) 

364 

365 module = AnsibleModule(argument_spec=argument_spec) 

366 

367 if HAS_FOOTMARK is False: 

368 module.fail_json(msg='footmark is required for the module ali_security_group.') 

369 ecs = ecs_connect(module) 

370 state = module.params['state'] 

371 security_group_id = module.params['security_group_id'] 

372 group_name = module.params['name'] 

373 if str(group_name).startswith('http://') or str(group_name).startswith('https://'): 

374 module.fail_json(msg='Name can not start with http:// or https://') 

375 description = module.params['description'] 

376 if str(description).startswith('http://') or str(description).startswith('https://'): 

377 module.fail_json(msg='description can not start with http:// or https://') 

378 multi = module.params['multi_ok'] 

379 recent = module.params['recent'] 

380 

381 if multi and recent: 

382 module.fail_json(msg='multi_ok and recent can not be True at the same time.') 

383 

384 changed = False 

385 

386 group = group_exists(ecs, module, module.params['vpc_id'], group_name, security_group_id, multi, recent) 

387 

388 if state == 'absent': 

389 if not group: 

390 module.exit_json(changed=changed, group={}) 

391 try: 

392 module.exit_json(changed=group.delete(), group={}) 

393 except ECSResponseError as e: 

394 module.fail_json(msg="Deleting security group {0} is failed. Error: {1}".format(group.id, e)) 

395 

396 if not group: 

397 try: 

398 params = module.params 

399 params['security_group_name'] = group_name 

400 params['client_token'] = "Ansible-Apsarastack-%s-%s" % (hash(str(module.params)), str(time.time())) 

401 group = ecs.create_security_group(**params) 

402 except ECSResponseError as e: 

403 module.fail_json(changed=changed, msg='Creating a security group is failed. Error: {0}'.format(e)) 

404 

405 if not description: 

406 description = group.description 

407 if group.modify(name=group_name, description=description): 

408 changed = True 

409 

410 # validating rules if provided 

411 ingress_rules = module.params['rules'] 

412 if ingress_rules: 

413 direction = 'ingress' 

414 for rule in ingress_rules: 

415 validate_group_rule_keys(module, rule, direction) 

416 if module.params['purge_rules']: 

417 for existing in group.permissions: 

418 if existing['direction'] != direction: 

419 continue 

420 if purge_rules(module, group, existing, ingress_rules, direction): 

421 changed = True 

422 for rule in ingress_rules: 

423 if group.authorize(rule, direction): 

424 changed = True 

425 

426 egress_rules = module.params['rules_egress'] 

427 if egress_rules: 

428 direction = 'egress' 

429 for rule in egress_rules: 

430 validate_group_rule_keys(module, rule, direction) 

431 if module.params['purge_rules_egress']: 

432 for existing in group.permissions: 

433 if existing['direction'] != direction: 

434 continue 

435 if purge_rules(module, group, existing, egress_rules, direction): 

436 changed = True 

437 for rule in egress_rules: 

438 if group.authorize(rule, direction): 

439 changed = True 

440 

441 module.exit_json(changed=changed, group=group.get().read()) 

442 

443 

444if __name__ == '__main__': 

445 main()