Files
community.general/plugins/modules/snmp_facts.py
Laurenz 5a27cbdec6 snmp_facts: update to pysnmp >= 7.1 async API (#11683)
* snmp_facts: update to pysnmp >= 7.1 async API

Migrate snmp_facts module from the removed pysnmp oneliner API
(pysnmp.entity.rfc3413.oneliner.cmdgen) to the current async API
(pysnmp.hlapi.v3arch.asyncio).

This fixes compatibility with Python 3.12+ and pysnmp >= 7.1.

Closes #8852

* Continue to support pysnmp 6.2.4

* Correct PR number

* sort imports

* shorter changelog

* move `SNMP_DEFAULT_PORT`

* Add `notes:`

* Become an author

* use `deps.declare`

* add lalten to BOTMETA
2026-03-30 21:50:49 +02:00

744 lines
24 KiB
Python

#!/usr/bin/python
# This file is part of Networklore's snmp library for Ansible
# Copyright (c) Ansible project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
DOCUMENTATION = r"""
module: snmp_facts
author:
- Patrick Ogenstad (@ogenstad)
- Laurenz Altenmueller (@lalten)
short_description: Retrieve facts for a device using SNMP
description:
- Retrieve facts for a device using SNMP. The facts are inserted to the RV(ansible_facts) key.
requirements:
- pysnmp (either pysnmp < 6.2.4 or pysnmp >= 7.1)
extends_documentation_fragment:
- community.general.attributes
- community.general.attributes.facts
- community.general.attributes.facts_module
attributes:
check_mode:
version_added: 3.3.0
# This was backported to 2.5.4 and 1.3.11 as well, since this was a bugfix
options:
host:
description:
- Set to target SNMP server (normally C({{ inventory_hostname }})).
type: str
required: true
version:
description:
- SNMP Version to use, V(v2), V(v2c) or V(v3).
type: str
required: true
choices: [v2, v2c, v3]
community:
description:
- The SNMP community string, required if O(version) is V(v2) or V(v2c).
type: str
level:
description:
- Authentication level.
- Required if O(version=v3).
type: str
choices: [authNoPriv, authPriv]
username:
description:
- Username for SNMPv3.
- Required if O(version=v3).
type: str
integrity:
description:
- Hashing algorithm.
- Required if O(version=v3).
type: str
choices: [md5, sha]
authkey:
description:
- Authentication key.
- Required O(version=v3).
type: str
privacy:
description:
- Encryption algorithm.
- Required if O(level=authPriv).
type: str
choices: [aes, des]
privkey:
description:
- Encryption key.
- Required if O(level=authPriv).
type: str
timeout:
description:
- Response timeout in seconds.
type: int
version_added: 2.3.0
retries:
description:
- Maximum number of request retries, 0 retries means just a single request.
type: int
version_added: 2.3.0
notes:
- Upgrading to pysnmp 7.1+ is recommended. Support for pysnmp 6.2 will be deprecated.
"""
EXAMPLES = r"""
- name: Gather facts with SNMP version 2
community.general.snmp_facts:
host: '{{ inventory_hostname }}'
version: v2c
community: public
delegate_to: local
- name: Gather facts using SNMP version 3
community.general.snmp_facts:
host: '{{ inventory_hostname }}'
version: v3
level: authPriv
integrity: sha
privacy: aes
username: snmp-user
authkey: abc12345
privkey: def6789
delegate_to: localhost
"""
RETURN = r"""
ansible_facts:
description: The returned facts.
returned: success
type: dict
contains:
ansible_sysdescr:
description: A textual description of the entity.
returned: success
type: str
sample: "Linux ubuntu-user 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017 x86_64"
ansible_sysobjectid:
description: The vendor's authoritative identification of the network management subsystem contained in the entity.
returned: success
type: str
sample: 1.3.6.1.4.1.8072.3.2.10
ansible_sysuptime:
description: The time (in hundredths of a second) since the network management portion of the system was last re-initialized.
returned: success
type: int
sample: 42388
ansible_syscontact:
description: The textual identification of the contact person for this managed node, together with information on how to
contact this person.
returned: success
type: str
sample: Me <me@example.org>
ansible_sysname:
description: An administratively-assigned name for this managed node.
returned: success
type: str
sample: ubuntu-user
ansible_syslocation:
description: The physical location of this node (for example, V(telephone closet, 3rd floor)).
returned: success
type: str
sample: Sitting on the Dock of the Bay
ansible_all_ipv4_addresses:
description: List of all IPv4 addresses.
returned: success
type: list
sample: ["127.0.0.1", "172.17.0.1"]
ansible_interfaces:
description: Dictionary of each network interface and its metadata.
returned: success
type: dict
sample:
{
"1": {
"adminstatus": "up",
"description": "",
"ifindex": "1",
"ipv4": [
{
"address": "127.0.0.1",
"netmask": "255.0.0.0"
}
],
"mac": "",
"mtu": "65536",
"name": "lo",
"operstatus": "up",
"speed": "65536"
},
"2": {
"adminstatus": "up",
"description": "",
"ifindex": "2",
"ipv4": [
{
"address": "192.168.213.128",
"netmask": "255.255.255.0"
}
],
"mac": "000a305a52a1",
"mtu": "1500",
"name": "Intel Corporation 82545EM Gigabit Ethernet Controller (Copper)",
"operstatus": "up",
"speed": "1500"
}
}
"""
import asyncio
import binascii
from collections import defaultdict
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.general.plugins.module_utils import deps
with deps.declare("pysnmp", url="https://pypi.org/project/pysnmp/"):
from pysnmp.hlapi.v3arch.asyncio import (
USM_AUTH_HMAC96_MD5,
USM_AUTH_HMAC96_SHA,
USM_PRIV_CBC56_DES,
USM_PRIV_CFB128_AES,
CommunityData,
ContextData,
ObjectIdentity,
ObjectType,
SnmpEngine,
UdpTransportTarget,
UsmUserData,
get_cmd,
next_cmd,
)
from pysnmp.proto.rfc1905 import EndOfMibView
with deps.declare("pysnmp6", url="https://pypi.org/project/pysnmp/"):
from pysnmp.entity.rfc3413.oneliner import cmdgen
from pysnmp.proto.rfc1905 import EndOfMibView
SNMP_DEFAULT_PORT = 161
class DefineOid:
def __init__(self, dotprefix=False):
if dotprefix:
dp = "."
else:
dp = ""
# From SNMPv2-MIB
self.sysDescr = f"{dp}1.3.6.1.2.1.1.1.0"
self.sysObjectId = f"{dp}1.3.6.1.2.1.1.2.0"
self.sysUpTime = f"{dp}1.3.6.1.2.1.1.3.0"
self.sysContact = f"{dp}1.3.6.1.2.1.1.4.0"
self.sysName = f"{dp}1.3.6.1.2.1.1.5.0"
self.sysLocation = f"{dp}1.3.6.1.2.1.1.6.0"
# From IF-MIB
self.ifIndex = f"{dp}1.3.6.1.2.1.2.2.1.1"
self.ifDescr = f"{dp}1.3.6.1.2.1.2.2.1.2"
self.ifMtu = f"{dp}1.3.6.1.2.1.2.2.1.4"
self.ifSpeed = f"{dp}1.3.6.1.2.1.2.2.1.5"
self.ifPhysAddress = f"{dp}1.3.6.1.2.1.2.2.1.6"
self.ifAdminStatus = f"{dp}1.3.6.1.2.1.2.2.1.7"
self.ifOperStatus = f"{dp}1.3.6.1.2.1.2.2.1.8"
self.ifAlias = f"{dp}1.3.6.1.2.1.31.1.1.1.18"
# From IP-MIB
self.ipAdEntAddr = f"{dp}1.3.6.1.2.1.4.20.1.1"
self.ipAdEntIfIndex = f"{dp}1.3.6.1.2.1.4.20.1.2"
self.ipAdEntNetMask = f"{dp}1.3.6.1.2.1.4.20.1.3"
def decode_hex(hexstring):
if len(hexstring) < 3:
return hexstring
if hexstring[:2] == "0x":
return to_text(binascii.unhexlify(hexstring[2:]))
return hexstring
def decode_mac(hexstring):
if len(hexstring) != 14:
return hexstring
if hexstring[:2] == "0x":
return hexstring[2:]
return hexstring
def lookup_adminstatus(int_adminstatus):
adminstatus_options = {1: "up", 2: "down", 3: "testing"}
return adminstatus_options.get(int_adminstatus, "")
def lookup_operstatus(int_operstatus):
operstatus_options = {
1: "up",
2: "down",
3: "testing",
4: "unknown",
5: "dormant",
6: "notPresent",
7: "lowerLayerDown",
}
return operstatus_options.get(int_operstatus, "")
def main():
module = AnsibleModule(
argument_spec=dict(
host=dict(type="str", required=True),
version=dict(type="str", required=True, choices=["v2", "v2c", "v3"]),
community=dict(type="str"),
username=dict(type="str"),
level=dict(type="str", choices=["authNoPriv", "authPriv"]),
integrity=dict(type="str", choices=["md5", "sha"]),
privacy=dict(type="str", choices=["aes", "des"]),
authkey=dict(type="str", no_log=True),
privkey=dict(type="str", no_log=True),
timeout=dict(type="int"),
retries=dict(type="int"),
),
required_together=(
["username", "level", "integrity", "authkey"],
["privacy", "privkey"],
),
required_if=[
("version", "v2", ["community"]),
("version", "v2c", ["community"]),
("version", "v3", ["username", "authkey", "level"]),
],
supports_check_mode=True,
)
m_args = module.params
if not deps.failed("pysnmp"):
return asyncio.run(_async_main(module, m_args))
deps.validate(module, "pysnmp6")
cmdGen = cmdgen.CommandGenerator()
transport_opts = {k: m_args[k] for k in ("timeout", "retries") if m_args[k] is not None}
integrity_proto = None
privacy_proto = None
if m_args["version"] == "v3":
if m_args["level"] == "authPriv" and m_args["privacy"] is None:
module.fail_json(msg="Privacy algorithm not set when using authPriv")
if m_args["integrity"] == "sha":
integrity_proto = cmdgen.usmHMACSHAAuthProtocol
elif m_args["integrity"] == "md5":
integrity_proto = cmdgen.usmHMACMD5AuthProtocol
if m_args["privacy"] == "aes":
privacy_proto = cmdgen.usmAesCfb128Protocol
elif m_args["privacy"] == "des":
privacy_proto = cmdgen.usmDESPrivProtocol
# Use SNMP Version 2
if m_args["version"] in ("v2", "v2c"):
snmp_auth = cmdgen.CommunityData(m_args["community"])
# Use SNMP Version 3 with authNoPriv
elif m_args["level"] == "authNoPriv":
snmp_auth = cmdgen.UsmUserData(m_args["username"], authKey=m_args["authkey"], authProtocol=integrity_proto)
# Use SNMP Version 3 with authPriv
else:
snmp_auth = cmdgen.UsmUserData(
m_args["username"],
authKey=m_args["authkey"],
privKey=m_args["privkey"],
authProtocol=integrity_proto,
privProtocol=privacy_proto,
)
# Use p to prefix OIDs with a dot for polling
p = DefineOid(dotprefix=True)
# Use v without a prefix to use with return values
v = DefineOid(dotprefix=False)
def Tree():
return defaultdict(Tree)
results = Tree()
errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
snmp_auth,
cmdgen.UdpTransportTarget((m_args["host"], 161), **transport_opts),
cmdgen.MibVariable(
p.sysDescr,
),
cmdgen.MibVariable(
p.sysObjectId,
),
cmdgen.MibVariable(
p.sysUpTime,
),
cmdgen.MibVariable(
p.sysContact,
),
cmdgen.MibVariable(
p.sysName,
),
cmdgen.MibVariable(
p.sysLocation,
),
lookupMib=False,
)
if errorIndication:
module.fail_json(msg=str(errorIndication))
for oid, val in varBinds:
current_oid = oid.prettyPrint()
current_val = val.prettyPrint()
if current_oid == v.sysDescr:
results["ansible_sysdescr"] = decode_hex(current_val)
elif current_oid == v.sysObjectId:
results["ansible_sysobjectid"] = current_val
elif current_oid == v.sysUpTime:
results["ansible_sysuptime"] = current_val
elif current_oid == v.sysContact:
results["ansible_syscontact"] = current_val
elif current_oid == v.sysName:
results["ansible_sysname"] = current_val
elif current_oid == v.sysLocation:
results["ansible_syslocation"] = current_val
errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd(
snmp_auth,
cmdgen.UdpTransportTarget((m_args["host"], 161), **transport_opts),
cmdgen.MibVariable(
p.ifIndex,
),
cmdgen.MibVariable(
p.ifDescr,
),
cmdgen.MibVariable(
p.ifMtu,
),
cmdgen.MibVariable(
p.ifSpeed,
),
cmdgen.MibVariable(
p.ifPhysAddress,
),
cmdgen.MibVariable(
p.ifAdminStatus,
),
cmdgen.MibVariable(
p.ifOperStatus,
),
cmdgen.MibVariable(
p.ipAdEntAddr,
),
cmdgen.MibVariable(
p.ipAdEntIfIndex,
),
cmdgen.MibVariable(
p.ipAdEntNetMask,
),
cmdgen.MibVariable(
p.ifAlias,
),
lookupMib=False,
)
if errorIndication:
module.fail_json(msg=str(errorIndication))
interface_indexes = []
all_ipv4_addresses = []
ipv4_networks = Tree()
for varBinds in varTable:
for oid, val in varBinds:
if isinstance(val, EndOfMibView):
continue
current_oid = oid.prettyPrint()
current_val = val.prettyPrint()
if v.ifIndex in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["ifindex"] = current_val
interface_indexes.append(ifIndex)
if v.ifDescr in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["name"] = current_val
if v.ifMtu in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["mtu"] = current_val
if v.ifSpeed in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["speed"] = current_val
if v.ifPhysAddress in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["mac"] = decode_mac(current_val)
if v.ifAdminStatus in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["adminstatus"] = lookup_adminstatus(int(current_val))
if v.ifOperStatus in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["operstatus"] = lookup_operstatus(int(current_val))
if v.ipAdEntAddr in current_oid:
curIPList = current_oid.rsplit(".", 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]["address"] = current_val
all_ipv4_addresses.append(current_val)
if v.ipAdEntIfIndex in current_oid:
curIPList = current_oid.rsplit(".", 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]["interface"] = current_val
if v.ipAdEntNetMask in current_oid:
curIPList = current_oid.rsplit(".", 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]["netmask"] = current_val
if v.ifAlias in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["description"] = current_val
interface_to_ipv4 = {}
for ipv4_network in ipv4_networks:
current_interface = ipv4_networks[ipv4_network]["interface"]
current_network = {
"address": ipv4_networks[ipv4_network]["address"],
"netmask": ipv4_networks[ipv4_network]["netmask"],
}
if current_interface not in interface_to_ipv4:
interface_to_ipv4[current_interface] = []
interface_to_ipv4[current_interface].append(current_network)
else:
interface_to_ipv4[current_interface].append(current_network)
for interface in interface_to_ipv4:
results["ansible_interfaces"][int(interface)]["ipv4"] = interface_to_ipv4[interface]
results["ansible_all_ipv4_addresses"] = all_ipv4_addresses
module.exit_json(ansible_facts=results)
async def _async_main(module, m_args):
"""SNMP facts retrieval using pysnmp >= 7.1 async v3arch API."""
transport_opts = {k: m_args[k] for k in ("timeout", "retries") if m_args[k] is not None}
integrity_proto = None
privacy_proto = None
if m_args["version"] == "v3":
if m_args["level"] == "authPriv" and m_args["privacy"] is None:
module.fail_json(msg="Privacy algorithm not set when using authPriv")
if m_args["integrity"] == "sha":
integrity_proto = USM_AUTH_HMAC96_SHA
elif m_args["integrity"] == "md5":
integrity_proto = USM_AUTH_HMAC96_MD5
if m_args["privacy"] == "aes":
privacy_proto = USM_PRIV_CFB128_AES
elif m_args["privacy"] == "des":
privacy_proto = USM_PRIV_CBC56_DES
# Use SNMP Version 2
if m_args["version"] in ("v2", "v2c"):
snmp_auth = CommunityData(m_args["community"])
# Use SNMP Version 3 with authNoPriv
elif m_args["level"] == "authNoPriv":
snmp_auth = UsmUserData(m_args["username"], authKey=m_args["authkey"], authProtocol=integrity_proto)
# Use SNMP Version 3 with authPriv
else:
snmp_auth = UsmUserData(
m_args["username"],
authKey=m_args["authkey"],
privKey=m_args["privkey"],
authProtocol=integrity_proto,
privProtocol=privacy_proto,
)
# Use p to prefix OIDs with a dot for polling
p = DefineOid(dotprefix=True)
# Use v without a prefix to use with return values
v = DefineOid(dotprefix=False)
def Tree():
return defaultdict(Tree)
results = Tree()
transport = await UdpTransportTarget.create((m_args["host"], SNMP_DEFAULT_PORT), **transport_opts)
snmpEngine = SnmpEngine()
errorIndication, errorStatus, errorIndex, varBinds = await get_cmd(
snmpEngine,
snmp_auth,
transport,
ContextData(),
ObjectType(ObjectIdentity(p.sysDescr)),
ObjectType(ObjectIdentity(p.sysObjectId)),
ObjectType(ObjectIdentity(p.sysUpTime)),
ObjectType(ObjectIdentity(p.sysContact)),
ObjectType(ObjectIdentity(p.sysName)),
ObjectType(ObjectIdentity(p.sysLocation)),
lookupMib=False,
)
if errorIndication:
module.fail_json(msg=str(errorIndication))
for oid, val in varBinds:
current_oid = oid.prettyPrint()
current_val = val.prettyPrint()
if current_oid == v.sysDescr:
results["ansible_sysdescr"] = decode_hex(current_val)
elif current_oid == v.sysObjectId:
results["ansible_sysobjectid"] = current_val
elif current_oid == v.sysUpTime:
results["ansible_sysuptime"] = current_val
elif current_oid == v.sysContact:
results["ansible_syscontact"] = current_val
elif current_oid == v.sysName:
results["ansible_sysname"] = current_val
elif current_oid == v.sysLocation:
results["ansible_syslocation"] = current_val
oids = [
ObjectType(ObjectIdentity(p.ifIndex)),
ObjectType(ObjectIdentity(p.ifDescr)),
ObjectType(ObjectIdentity(p.ifMtu)),
ObjectType(ObjectIdentity(p.ifSpeed)),
ObjectType(ObjectIdentity(p.ifPhysAddress)),
ObjectType(ObjectIdentity(p.ifAdminStatus)),
ObjectType(ObjectIdentity(p.ifOperStatus)),
ObjectType(ObjectIdentity(p.ipAdEntAddr)),
ObjectType(ObjectIdentity(p.ipAdEntIfIndex)),
ObjectType(ObjectIdentity(p.ipAdEntNetMask)),
ObjectType(ObjectIdentity(p.ifAlias)),
]
base_oids = [
v.ifIndex,
v.ifDescr,
v.ifMtu,
v.ifSpeed,
v.ifPhysAddress,
v.ifAdminStatus,
v.ifOperStatus,
v.ipAdEntAddr,
v.ipAdEntIfIndex,
v.ipAdEntNetMask,
v.ifAlias,
]
varTable = []
while True:
errorIndication, errorStatus, errorIndex, varBinds_walk = await next_cmd(
snmpEngine,
snmp_auth,
transport,
ContextData(),
*oids,
lookupMib=False,
)
if errorIndication:
module.fail_json(msg=str(errorIndication))
if errorStatus:
break
# Stop when all OIDs have left their base subtree or hit EndOfMibView
if all(
isinstance(vb[1], EndOfMibView) or not vb[0].prettyPrint().startswith(base + ".")
for vb, base in zip(varBinds_walk, base_oids)
):
break
varTable.append(varBinds_walk)
oids = varBinds_walk
interface_indexes = []
all_ipv4_addresses = []
ipv4_networks = Tree()
for varBinds in varTable:
for oid, val in varBinds:
if isinstance(val, EndOfMibView):
continue
current_oid = oid.prettyPrint()
current_val = val.prettyPrint()
if v.ifIndex in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["ifindex"] = current_val
interface_indexes.append(ifIndex)
if v.ifDescr in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["name"] = current_val
if v.ifMtu in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["mtu"] = current_val
if v.ifSpeed in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["speed"] = current_val
if v.ifPhysAddress in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["mac"] = decode_mac(current_val)
if v.ifAdminStatus in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["adminstatus"] = lookup_adminstatus(int(current_val))
if v.ifOperStatus in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["operstatus"] = lookup_operstatus(int(current_val))
if v.ipAdEntAddr in current_oid:
curIPList = current_oid.rsplit(".", 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]["address"] = current_val
all_ipv4_addresses.append(current_val)
if v.ipAdEntIfIndex in current_oid:
curIPList = current_oid.rsplit(".", 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]["interface"] = current_val
if v.ipAdEntNetMask in current_oid:
curIPList = current_oid.rsplit(".", 4)[-4:]
curIP = ".".join(curIPList)
ipv4_networks[curIP]["netmask"] = current_val
if v.ifAlias in current_oid:
ifIndex = int(current_oid.rsplit(".", 1)[-1])
results["ansible_interfaces"][ifIndex]["description"] = current_val
interface_to_ipv4 = {}
for ipv4_network in ipv4_networks:
current_interface = ipv4_networks[ipv4_network]["interface"]
current_network = {
"address": ipv4_networks[ipv4_network]["address"],
"netmask": ipv4_networks[ipv4_network]["netmask"],
}
if current_interface not in interface_to_ipv4:
interface_to_ipv4[current_interface] = []
interface_to_ipv4[current_interface].append(current_network)
else:
interface_to_ipv4[current_interface].append(current_network)
for interface in interface_to_ipv4:
results["ansible_interfaces"][int(interface)]["ipv4"] = interface_to_ipv4[interface]
results["ansible_all_ipv4_addresses"] = all_ipv4_addresses
module.exit_json(ansible_facts=results)
if __name__ == "__main__":
main()