1#!/usr/bin/python 2 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8ANSIBLE_METADATA = {'metadata_version': '1.1', 9 'status': ['preview'], 10 'supported_by': 'community'} 11 12 13DOCUMENTATION = """ 14--- 15module: routeros_facts 16version_added: "2.8" 17author: "Egor Zaitsev (@heuels)" 18short_description: Collect facts from remote devices running MikroTik RouterOS 19description: 20 - Collects a base set of device facts from a remote device that 21 is running RotuerOS. This module prepends all of the 22 base network fact keys with C(ansible_net_<fact>). The facts 23 module will always collect a base set of facts from the device 24 and can enable or disable collection of additional facts. 25options: 26 gather_subset: 27 description: 28 - When supplied, this argument will restrict the facts collected 29 to a given subset. Possible values for this argument include 30 C(all), C(hardware), C(config), and C(interfaces). Can specify a list of 31 values to include a larger subset. Values can also be used 32 with an initial C(!) to specify that a specific subset should 33 not be collected. 34 required: false 35 default: '!config' 36""" 37 38EXAMPLES = """ 39# Collect all facts from the device 40- routeros_facts: 41 gather_subset: all 42 43# Collect only the config and default facts 44- routeros_facts: 45 gather_subset: 46 - config 47 48# Do not collect hardware facts 49- routeros_facts: 50 gather_subset: 51 - "!hardware" 52""" 53 54RETURN = """ 55ansible_net_gather_subset: 56 description: The list of fact subsets collected from the device 57 returned: always 58 type: list 59 60# default 61ansible_net_model: 62 description: The model name returned from the device 63 returned: always 64 type: str 65ansible_net_serialnum: 66 description: The serial number of the remote device 67 returned: always 68 type: str 69ansible_net_version: 70 description: The operating system version running on the remote device 71 returned: always 72 type: str 73ansible_net_hostname: 74 description: The configured hostname of the device 75 returned: always 76 type: str 77 78# hardware 79ansible_net_spacefree_mb: 80 description: The available disk space on the remote device in MiB 81 returned: when hardware is configured 82 type: dict 83ansible_net_spacetotal_mb: 84 description: The total disk space on the remote device in MiB 85 returned: when hardware is configured 86 type: dict 87ansible_net_memfree_mb: 88 description: The available free memory on the remote device in MiB 89 returned: when hardware is configured 90 type: int 91ansible_net_memtotal_mb: 92 description: The total memory on the remote device in MiB 93 returned: when hardware is configured 94 type: int 95 96# config 97ansible_net_config: 98 description: The current active config from the device 99 returned: when config is configured 100 type: str 101 102# interfaces 103ansible_net_all_ipv4_addresses: 104 description: All IPv4 addresses configured on the device 105 returned: when interfaces is configured 106 type: list 107ansible_net_all_ipv6_addresses: 108 description: All IPv6 addresses configured on the device 109 returned: when interfaces is configured 110 type: list 111ansible_net_interfaces: 112 description: A hash of all interfaces running on the system 113 returned: when interfaces is configured 114 type: dict 115ansible_net_neighbors: 116 description: The list of neighbors from the remote device 117 returned: when interfaces is configured 118 type: dict 119""" 120import re 121 122from ansible.module_utils.network.routeros.routeros import run_commands 123from ansible.module_utils.network.routeros.routeros import routeros_argument_spec 124from ansible.module_utils.basic import AnsibleModule 125from ansible.module_utils.six import iteritems 126 127 128class FactsBase(object): 129 130 COMMANDS = list() 131 132 def __init__(self, module): 133 self.module = module 134 self.facts = dict() 135 self.responses = None 136 137 def populate(self): 138 self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False) 139 140 def run(self, cmd): 141 return run_commands(self.module, commands=cmd, check_rc=False) 142 143 144class Default(FactsBase): 145 146 COMMANDS = [ 147 '/system identity print without-paging', 148 '/system resource print without-paging', 149 '/system routerboard print without-paging' 150 ] 151 152 def populate(self): 153 super(Default, self).populate() 154 data = self.responses[0] 155 if data: 156 self.facts['hostname'] = self.parse_hostname(data) 157 158 data = self.responses[1] 159 if data: 160 self.facts['version'] = self.parse_version(data) 161 162 data = self.responses[2] 163 if data: 164 self.facts['model'] = self.parse_model(data) 165 self.facts['serialnum'] = self.parse_serialnum(data) 166 167 def parse_hostname(self, data): 168 match = re.search(r'name:\s(.*)\s*$', data, re.M) 169 if match: 170 return match.group(1) 171 172 def parse_version(self, data): 173 match = re.search(r'version:\s(.*)\s*$', data, re.M) 174 if match: 175 return match.group(1) 176 177 def parse_model(self, data): 178 match = re.search(r'model:\s(.*)\s*$', data, re.M) 179 if match: 180 return match.group(1) 181 182 def parse_serialnum(self, data): 183 match = re.search(r'serial-number:\s(.*)\s*$', data, re.M) 184 if match: 185 return match.group(1) 186 187 188class Hardware(FactsBase): 189 190 COMMANDS = [ 191 '/system resource print without-paging' 192 ] 193 194 def populate(self): 195 super(Hardware, self).populate() 196 data = self.responses[0] 197 if data: 198 self.parse_filesystem_info(data) 199 self.parse_memory_info(data) 200 201 def parse_filesystem_info(self, data): 202 match = re.search(r'free-hdd-space:\s(.*)([KMG]iB)', data, re.M) 203 if match: 204 self.facts['spacefree_mb'] = self.to_megabytes(match) 205 match = re.search(r'total-hdd-space:\s(.*)([KMG]iB)', data, re.M) 206 if match: 207 self.facts['spacetotal_mb'] = self.to_megabytes(match) 208 209 def parse_memory_info(self, data): 210 match = re.search(r'free-memory:\s(\d+\.?\d*)([KMG]iB)', data, re.M) 211 if match: 212 self.facts['memfree_mb'] = self.to_megabytes(match) 213 match = re.search(r'total-memory:\s(\d+\.?\d*)([KMG]iB)', data, re.M) 214 if match: 215 self.facts['memtotal_mb'] = self.to_megabytes(match) 216 217 def to_megabytes(self, data): 218 if data.group(2) == 'KiB': 219 return float(data.group(1)) / 1024 220 elif data.group(2) == 'MiB': 221 return float(data.group(1)) 222 elif data.group(2) == 'GiB': 223 return float(data.group(1)) * 1024 224 else: 225 return None 226 227 228class Config(FactsBase): 229 230 COMMANDS = ['/export'] 231 232 def populate(self): 233 super(Config, self).populate() 234 data = self.responses[0] 235 if data: 236 self.facts['config'] = data 237 238 239class Interfaces(FactsBase): 240 241 COMMANDS = [ 242 '/interface print detail without-paging', 243 '/ip address print detail without-paging', 244 '/ipv6 address print detail without-paging', 245 '/ip neighbor print detail without-paging' 246 ] 247 248 DETAIL_RE = re.compile(r'([\w\d\-]+)=\"?(\w{3}/\d{2}/\d{4}\s\d{2}:\d{2}:\d{2}|[\w\d\-\.:/]+)') 249 WRAPPED_LINE_RE = re.compile(r'^\s+(?!\d)') 250 251 def populate(self): 252 super(Interfaces, self).populate() 253 254 self.facts['interfaces'] = dict() 255 self.facts['all_ipv4_addresses'] = list() 256 self.facts['all_ipv6_addresses'] = list() 257 self.facts['neighbors'] = dict() 258 259 data = self.responses[0] 260 if data: 261 interfaces = self.parse_interfaces(data) 262 self.populate_interfaces(interfaces) 263 264 data = self.responses[1] 265 if data: 266 data = self.parse_addresses(data) 267 self.populate_ipv4_interfaces(data) 268 269 data = self.responses[2] 270 if data: 271 data = self.parse_addresses(data) 272 self.populate_ipv6_interfaces(data) 273 274 data = self.responses[3] 275 if data: 276 self.facts['neighbors'] = self.parse_neighbors(data) 277 278 def populate_interfaces(self, data): 279 for key, value in iteritems(data): 280 self.facts['interfaces'][key] = value 281 282 def populate_ipv4_interfaces(self, data): 283 for key, value in iteritems(data): 284 if 'ipv4' not in self.facts['interfaces'][key]: 285 self.facts['interfaces'][key]['ipv4'] = list() 286 addr, subnet = value['address'].split("/") 287 ipv4 = dict(address=addr.strip(), subnet=subnet.strip()) 288 self.add_ip_address(addr.strip(), 'ipv4') 289 self.facts['interfaces'][key]['ipv4'].append(ipv4) 290 291 def populate_ipv6_interfaces(self, data): 292 for key, value in iteritems(data): 293 if key is None: 294 break 295 if 'ipv6' not in self.facts['interfaces'][key]: 296 self.facts['interfaces'][key]['ipv6'] = list() 297 addr, subnet = value['address'].split("/") 298 ipv6 = dict(address=addr.strip(), subnet=subnet.strip()) 299 self.add_ip_address(addr.strip(), 'ipv6') 300 self.facts['interfaces'][key]['ipv6'].append(ipv6) 301 302 def add_ip_address(self, address, family): 303 if family == 'ipv4': 304 self.facts['all_ipv4_addresses'].append(address) 305 else: 306 self.facts['all_ipv6_addresses'].append(address) 307 308 def preprocess(self, data): 309 preprocessed = list() 310 for line in data.split('\n'): 311 if len(line) == 0 or line[:5] == 'Flags': 312 continue 313 elif not re.match(self.WRAPPED_LINE_RE, line): 314 preprocessed.append(line) 315 else: 316 preprocessed[-1] += line 317 return preprocessed 318 319 def parse_interfaces(self, data): 320 facts = dict() 321 data = self.preprocess(data) 322 for line in data: 323 name = self.parse_name(line) 324 facts[name] = dict() 325 for (key, value) in re.findall(self.DETAIL_RE, line): 326 facts[name][key] = value 327 return facts 328 329 def parse_addresses(self, data): 330 facts = dict() 331 data = self.preprocess(data) 332 for line in data: 333 name = self.parse_interface(line) 334 facts[name] = dict() 335 for (key, value) in re.findall(self.DETAIL_RE, line): 336 facts[name][key] = value 337 return facts 338 339 def parse_neighbors(self, data): 340 facts = dict() 341 data = self.preprocess(data) 342 for line in data: 343 name = self.parse_interface(line) 344 facts[name] = dict() 345 for (key, value) in re.findall(self.DETAIL_RE, line): 346 facts[name][key] = value 347 return facts 348 349 def parse_name(self, data): 350 match = re.search(r'name=\"([\w\d\-]+)\"', data, re.M) 351 if match: 352 return match.group(1) 353 354 def parse_interface(self, data): 355 match = re.search(r'interface=([\w\d\-]+)', data, re.M) 356 if match: 357 return match.group(1) 358 359 360FACT_SUBSETS = dict( 361 default=Default, 362 hardware=Hardware, 363 interfaces=Interfaces, 364 config=Config, 365) 366 367VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 368 369warnings = list() 370 371 372def main(): 373 """main entry point for module execution 374 """ 375 argument_spec = dict( 376 gather_subset=dict(default=['!config'], type='list') 377 ) 378 379 argument_spec.update(routeros_argument_spec) 380 381 module = AnsibleModule(argument_spec=argument_spec, 382 supports_check_mode=True) 383 384 gather_subset = module.params['gather_subset'] 385 386 runable_subsets = set() 387 exclude_subsets = set() 388 389 for subset in gather_subset: 390 if subset == 'all': 391 runable_subsets.update(VALID_SUBSETS) 392 continue 393 394 if subset.startswith('!'): 395 subset = subset[1:] 396 if subset == 'all': 397 exclude_subsets.update(VALID_SUBSETS) 398 continue 399 exclude = True 400 else: 401 exclude = False 402 403 if subset not in VALID_SUBSETS: 404 module.fail_json(msg='Bad subset: %s' % subset) 405 406 if exclude: 407 exclude_subsets.add(subset) 408 else: 409 runable_subsets.add(subset) 410 411 if not runable_subsets: 412 runable_subsets.update(VALID_SUBSETS) 413 414 runable_subsets.difference_update(exclude_subsets) 415 runable_subsets.add('default') 416 417 facts = dict() 418 facts['gather_subset'] = list(runable_subsets) 419 420 instances = list() 421 for key in runable_subsets: 422 instances.append(FACT_SUBSETS[key](module)) 423 424 for inst in instances: 425 inst.populate() 426 facts.update(inst.facts) 427 428 ansible_facts = dict() 429 for key, value in iteritems(facts): 430 key = 'ansible_net_%s' % key 431 ansible_facts[key] = value 432 433 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 434 435 436if __name__ == '__main__': 437 main() 438