zscams 2.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. zscams/__init__.py +0 -0
  2. zscams/__main__.py +101 -0
  3. zscams/agent/__init__.py +0 -0
  4. zscams/agent/certificates/.gitkeep +0 -0
  5. zscams/agent/config.yaml +121 -0
  6. zscams/agent/keys/autoport.key +27 -0
  7. zscams/agent/src/__init__.py +0 -0
  8. zscams/agent/src/core/__init__.py +1 -0
  9. zscams/agent/src/core/api/backend/bootstrap.py +21 -0
  10. zscams/agent/src/core/api/backend/client.py +242 -0
  11. zscams/agent/src/core/api/backend/exceptions.py +10 -0
  12. zscams/agent/src/core/api/backend/update_machine_info.py +16 -0
  13. zscams/agent/src/core/prerequisites.py +36 -0
  14. zscams/agent/src/core/service_health_check.py +49 -0
  15. zscams/agent/src/core/services.py +86 -0
  16. zscams/agent/src/core/tunnel/__init__.py +144 -0
  17. zscams/agent/src/core/tunnel/tls.py +56 -0
  18. zscams/agent/src/core/tunnels.py +55 -0
  19. zscams/agent/src/services/__init__.py +0 -0
  20. zscams/agent/src/services/reverse_ssh.py +73 -0
  21. zscams/agent/src/services/ssh_forwarder.py +75 -0
  22. zscams/agent/src/services/system_monitor.py +264 -0
  23. zscams/agent/src/support/__init__.py +0 -0
  24. zscams/agent/src/support/configuration.py +53 -0
  25. zscams/agent/src/support/filesystem.py +41 -0
  26. zscams/agent/src/support/logger.py +40 -0
  27. zscams/agent/src/support/mac.py +18 -0
  28. zscams/agent/src/support/network.py +49 -0
  29. zscams/agent/src/support/openssl.py +100 -0
  30. zscams/libs/getmac/__init__.py +3 -0
  31. zscams/libs/getmac/__main__.py +123 -0
  32. zscams/libs/getmac/getmac.py +1900 -0
  33. zscams/libs/getmac/shutilwhich.py +67 -0
  34. zscams-2.0.1.dist-info/METADATA +118 -0
  35. zscams-2.0.1.dist-info/RECORD +37 -0
  36. zscams-2.0.1.dist-info/WHEEL +4 -0
  37. zscams-2.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,264 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ import datetime
5
+ import psutil
6
+ import logging
7
+ import logging.handlers
8
+ import json
9
+ import socket
10
+ import platform
11
+ from zscams.agent.src.support.logger import get_logger
12
+ from http.client import BadStatusLine, HTTPConnection, HTTPException
13
+ logger = get_logger("system_monitor")
14
+
15
+ # Load service-specific params from environment
16
+ params_env = os.environ.get("SERVICE_PARAMS")
17
+ if not params_env:
18
+ logger.error("SERVICE_PARAMS environment variable not set")
19
+ sys.exit(1)
20
+
21
+ params = json.loads(params_env)
22
+ # -----------------------------
23
+ # Configuration
24
+ # -----------------------------
25
+ SYSLOG_HOST = params.get("remote_host", "localhost") # remote rsyslog host
26
+ SYSLOG_PORT = params.get("remote_port", 514) # TCP syslog port
27
+ EQUIPMENT_NAME = params.get("equipment_name", "connector")
28
+ EQUIPMENT_TYPE = params.get("equipment_type", "zpa")
29
+ SERVICE_NAME = params.get("service_name", "Zscaler-AppConnector")
30
+ HOSTNAME = platform.node()
31
+
32
+
33
+
34
+
35
+ def network():
36
+ """
37
+ Collect network interfaces and their statistics.
38
+ Returns a dictionary with per-interface
39
+ """
40
+ ifaces = {}
41
+ for iface, stats in psutil.net_io_counters(pernic=True).items():
42
+ ifaces[iface] = {
43
+ "in": {
44
+ "bytes": stats.bytes_recv,
45
+ "packets": stats.packets_recv,
46
+ "errors": getattr(stats, "errin", 0),
47
+ "dropped": getattr(stats, "dropin", 0),
48
+ },
49
+ "out": {
50
+ "bytes": stats.bytes_sent,
51
+ "packets": stats.packets_sent,
52
+ "errors": getattr(stats, "errout", 0),
53
+ "dropped": getattr(stats, "dropout", 0),
54
+ },
55
+ }
56
+ if "lo" in ifaces:
57
+ del ifaces["lo"]
58
+ return ifaces
59
+
60
+
61
+ def memory():
62
+ """
63
+ Get memory and swap usage
64
+ Returns a dictionary with total, used, free, and swap info.
65
+ """
66
+ mem = psutil.virtual_memory()
67
+ swap = psutil.swap_memory()
68
+ mem_remain = mem.total - mem.available
69
+ output_memory = {
70
+ "actual": {
71
+ "free": mem.available,
72
+ "used": {
73
+ "pct": round(mem_remain / mem.total, 4),
74
+ "bytes": mem_remain,
75
+ },
76
+ },
77
+ "total": mem.total,
78
+ "used": {
79
+ "pct": round(mem.percent / 100, 4),
80
+ "bytes": mem.total - mem.available,
81
+ },
82
+ "free": mem.available,
83
+ "swap": {
84
+ "total": swap.total,
85
+ "used": {
86
+ "pct": swap.percent / 100,
87
+ "bytes": swap.used,
88
+ },
89
+ "free": swap.free,
90
+ },
91
+ }
92
+ return output_memory
93
+
94
+
95
+ def cpu():
96
+ """
97
+ Get CPU usage
98
+ Returns a dictionary with total, user, system, idle, and core count.
99
+ """
100
+ times = psutil.cpu_times()
101
+ total = sum(times)
102
+ used = total - times.idle
103
+ cpu_total_pct = round(used / total, 4)
104
+ return {
105
+ "total": {"pct": cpu_total_pct},
106
+ "system": {"pct": round(times.system / total, 4)},
107
+ "user": {"pct": round(times.user / total, 4)},
108
+ "idle": {"pct": round(times.idle / total, 4)},
109
+ "cores": psutil.cpu_count(),
110
+ "iowait": {"pct": round(getattr(times, "iowait", 0) / total, 4)},
111
+ "irq": {"pct": round(getattr(times, "irq", 0) / total, 4)},
112
+ "softirq": {"pct": round(getattr(times, "softirq", 0) / total, 4)},
113
+ "nice": {"pct": round(getattr(times, "nice", 0) / total, 4)},
114
+ "steal": {"pct": round(getattr(times, "steal", 0) / total, 4)},
115
+ }
116
+
117
+
118
+ def load():
119
+ """
120
+ Get CPU load averages over 1, 5, and 15 minutes.
121
+ Returns a dictionary with normalized load per CPU core.
122
+ """
123
+ load1, load5, load15 = psutil.getloadavg()
124
+ cores = psutil.cpu_count()
125
+ cores = float(cores) if cores else 1.0
126
+ return {
127
+ "1": load1,
128
+ "5": load5,
129
+ "15": load15,
130
+ "cores": cores,
131
+ "norm": {
132
+ "1": load1 / cores,
133
+ "5": load5 / cores,
134
+ "15": load15 / cores,
135
+ },
136
+ }
137
+
138
+
139
+ def uptime():
140
+ """
141
+ Return system boot time in seconds since the epoch.
142
+ """
143
+ return {"duration": {"seconds": int(psutil.boot_time())}}
144
+
145
+
146
+ def disk():
147
+ """
148
+ Get disk usage metrics for the root filesystem.
149
+ Returns a dictionary with total, used, and free bytes.
150
+ """
151
+ disk = psutil.disk_usage("/")
152
+ return {"total": disk.total, "used": disk.used, "free": disk.free}
153
+
154
+
155
+ def process(process_names=None):
156
+ """
157
+ Get metrics about running processes by name.
158
+ Default: ["zpa-connector", "zpa-service-edge"]
159
+ Returns a list of dictionaries with process info.
160
+ """
161
+ if process_names is None:
162
+ process_names = ["zpa-connector", "zpa-service-edge"]
163
+
164
+ output = []
165
+ for proc in psutil.process_iter(
166
+ [
167
+ "name",
168
+ "pid",
169
+ "ppid",
170
+ "username",
171
+ "memory_info",
172
+ "cpu_times",
173
+ "cmdline",
174
+ "status",
175
+ "create_time",
176
+ ]
177
+ ):
178
+ try:
179
+ if proc.info["name"] in process_names:
180
+ mem = proc.info["memory_info"]
181
+ cpu_pct = proc.cpu_percent(interval=0.1)
182
+ output.append(
183
+ {
184
+ "name": proc.info["name"],
185
+ "pid": proc.info["pid"],
186
+ "ppid": proc.info["ppid"],
187
+ "username": proc.info["username"],
188
+ "memory": {
189
+ "rss": mem.rss,
190
+ "vms": mem.vms,
191
+ "percent": round(proc.memory_percent(), 2),
192
+ },
193
+ "cpu": {
194
+ "percent": cpu_pct,
195
+ "start_time": datetime.datetime.fromtimestamp(
196
+ proc.info["create_time"]
197
+ ).isoformat(),
198
+ },
199
+ "cmdline": proc.info["cmdline"],
200
+ "status": proc.info["status"],
201
+ }
202
+ )
203
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
204
+ continue
205
+ return output
206
+
207
+
208
+ def collect_all():
209
+ """
210
+ Collect all metrics in a single dictionary.
211
+ """
212
+ return {
213
+ "network": network(),
214
+ "memory": memory(),
215
+ "cpu": cpu(),
216
+ "load": load(),
217
+ "uptime": uptime(),
218
+ "disk": disk(),
219
+ "processes": process(),
220
+ }
221
+
222
+
223
+ def log():
224
+ return {
225
+ "system": collect_all(),
226
+ "beat": {
227
+ "name": EQUIPMENT_NAME,
228
+ "type": EQUIPMENT_TYPE,
229
+ "hostname": HOSTNAME,
230
+ },
231
+ "host": HOSTNAME,
232
+ }
233
+
234
+
235
+
236
+ # -----------------------------
237
+ # Helper function to log JSON
238
+ # -----------------------------
239
+ async def send_json_log(payload: dict):
240
+ """
241
+ Send a JSON-formatted log to syslog via TCP
242
+ """
243
+ conn = HTTPConnection(SYSLOG_HOST, SYSLOG_PORT, timeout=10)
244
+ conn.request(
245
+ "POST", "/", json.dumps(payload), headers={"Content-type": "application/json"}
246
+ )
247
+ conn.close()
248
+ logger.info("Metrices collected and sent successfully.")
249
+
250
+
251
+ async def schedule_task(interval):
252
+ while (True):
253
+ try:
254
+ await send_json_log(log())
255
+ except Exception as exception:
256
+ print(exception)
257
+ await asyncio.sleep(interval)
258
+
259
+ if __name__ == "__main__":
260
+ try:
261
+ asyncio.run(schedule_task(30))
262
+ except KeyboardInterrupt:
263
+ logger.info("Exiting system_monitor.py")
264
+ sys.exit(0)
File without changes
@@ -0,0 +1,53 @@
1
+ """
2
+ Configuration loader module
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import TypedDict
8
+
9
+ import yaml
10
+
11
+
12
+ config = {}
13
+
14
+ ROOT_PATH = Path(__file__).parent.parent.parent
15
+
16
+ CONFIG_PATH = os.path.join(ROOT_PATH.absolute(), "config.yaml")
17
+
18
+
19
+ class RemoteConfig(TypedDict):
20
+ """Type definition for remote configuration."""
21
+
22
+ host: str
23
+ port: str
24
+ verify_cert: bool
25
+ client_key: str
26
+ ca_cert: str
27
+ ca_chain: str
28
+
29
+
30
+ def load_config():
31
+ """
32
+ Load and parse the YAML configuration file.
33
+
34
+ Returns:
35
+ dict: Configuration dictionary containing remote settings and forwards.
36
+ """
37
+ with open(CONFIG_PATH, "r") as f:
38
+ config = yaml.safe_load(f)
39
+ return config
40
+
41
+
42
+ def get_config():
43
+ """
44
+ Get the loaded configuration.
45
+
46
+ Returns:
47
+ dict: Configuration dictionary containing remote settings and forwards.
48
+ """
49
+
50
+ if not config:
51
+ return load_config()
52
+
53
+ return config
@@ -0,0 +1,41 @@
1
+ """
2
+ Path utilities for TLS Tunnel Client
3
+ """
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+
9
+ def resolve_path(path: Optional[str], base_dir: Optional[str] = None) -> Optional[str]:
10
+ """
11
+ Resolve a file path relative to a base directory.
12
+
13
+ Args:
14
+ path (str): Path to resolve (absolute or relative)
15
+ base_dir (str, optional): Directory to resolve relative paths from.
16
+ Defaults to None (current working directory)
17
+
18
+ Returns:
19
+ str: Absolute path
20
+ """
21
+ if path is None:
22
+ return None
23
+ if not os.path.isabs(path) and base_dir:
24
+ return os.path.join(base_dir, path)
25
+ return path
26
+
27
+
28
+ def ensure_dir(path: str):
29
+ """Ensure a directory exists."""
30
+
31
+ os.makedirs(path, exist_ok=True)
32
+
33
+
34
+ def is_file_exists(path, logger):
35
+ """Check if file exists."""
36
+ if os.path.exists(path):
37
+ logger.debug(f"File exists: {path}")
38
+ return True
39
+
40
+ logger.error(f"File not found: {path}")
41
+ return False
@@ -0,0 +1,40 @@
1
+ """
2
+ Logger module for TLS Tunnel Client
3
+ """
4
+
5
+ import logging
6
+ import sys
7
+
8
+ from zscams.agent.src.support.configuration import get_config
9
+
10
+
11
+ loggers: dict[str, logging.Logger] = {}
12
+
13
+
14
+ def get_logger(name: str = "tls_tunnel") -> logging.Logger:
15
+ """Create a singleton instance for that logger name"""
16
+
17
+ if name in loggers:
18
+ return loggers[name]
19
+
20
+ logger = logging.getLogger(name)
21
+
22
+ level = get_config().get("logging", {}).get("level", 10)
23
+
24
+ logger.setLevel(level)
25
+ logger.propagate = False
26
+
27
+ if logger.hasHandlers():
28
+ logger.handlers.clear()
29
+
30
+ console_handler = logging.StreamHandler(sys.stdout)
31
+ console_handler.setLevel(level)
32
+
33
+ console_formatter = logging.Formatter(
34
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s", "%Y-%m-%d %H:%M:%S"
35
+ )
36
+ console_handler.setFormatter(console_formatter)
37
+ logger.addHandler(console_handler)
38
+
39
+ loggers[name] = logger
40
+ return logger
@@ -0,0 +1,18 @@
1
+ """Module to retrieve the MAC address of the primary network interface."""
2
+
3
+ from zscams.libs.getmac import get_mac_address as gma
4
+
5
+
6
+ class MacAddressError(Exception):
7
+ """Exception raised when MAC address cannot be retrieved."""
8
+
9
+ pass
10
+
11
+
12
+ def get_mac_address() -> str:
13
+ """Get the MAC address of the primary network interface."""
14
+
15
+ mac_addr = gma()
16
+ if not mac_addr:
17
+ raise MacAddressError("Could not retrieve MAC address.")
18
+ return mac_addr
@@ -0,0 +1,49 @@
1
+ """
2
+ Network utilities for TLS Tunnel Client
3
+ """
4
+
5
+ import socket
6
+
7
+
8
+ def is_port_open(host, port, logger):
9
+ """Check if TCP port is open."""
10
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
11
+ sock.settimeout(1)
12
+ try:
13
+ sock.connect((host, port))
14
+ logger.debug(f"Port {port} on {host} is open.")
15
+ return True
16
+ except (ConnectionRefusedError, OSError):
17
+ logger.error(f"Port {port} on {host} is not open.")
18
+ return False
19
+ except Exception as err:
20
+ logger.error("Port check failed for %s:%s. Error: %s", host, port, err)
21
+ return False
22
+
23
+
24
+ def is_service_running(service_name, running_services, logger):
25
+ """Wait for another service to be marked as running."""
26
+ if service_name in running_services:
27
+ logger.debug(f"Service {service_name} is running.")
28
+ return True
29
+
30
+ logger.error(f"Service {service_name} is not running.")
31
+ return False
32
+
33
+
34
+ def get_local_ip_address():
35
+ """Get the local IP address of the machine."""
36
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
37
+ try:
38
+ # The IP used here doesn't need to be reachable
39
+ s.connect(("8.8.8.8", 80))
40
+ ip = s.getsockname()[0]
41
+ except Exception:
42
+ ip = "127.0.0.1"
43
+ return ip
44
+
45
+
46
+ def get_local_hostname():
47
+ """Get the local hostname of the machine."""
48
+
49
+ return socket.gethostname()
@@ -0,0 +1,100 @@
1
+ """
2
+ This module provides a simple interface for generating RSA key pairs
3
+ """
4
+
5
+ from cryptography import x509
6
+ from cryptography.hazmat.primitives.asymmetric import rsa
7
+ from cryptography.x509.oid import NameOID
8
+ from cryptography.hazmat.primitives import hashes, serialization
9
+ from cryptography.hazmat.backends import default_backend
10
+
11
+
12
+ def generate_private_key(key_path: str):
13
+ """Generate a new RSA private key and save it to a file."""
14
+
15
+ private_key = rsa.generate_private_key(
16
+ public_exponent=65537,
17
+ key_size=4096,
18
+ )
19
+
20
+ pem_private_key = private_key.private_bytes(
21
+ encoding=serialization.Encoding.PEM,
22
+ format=serialization.PrivateFormat.PKCS8,
23
+ encryption_algorithm=serialization.NoEncryption(),
24
+ )
25
+
26
+ with open(key_path, "wb") as f:
27
+ f.write(pem_private_key)
28
+
29
+ return pem_private_key
30
+
31
+
32
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
33
+ def generate_csr_from_private_key(
34
+ private_key_content: bytes,
35
+ country: str,
36
+ state: str,
37
+ locality: str,
38
+ organization: str,
39
+ common_name: str,
40
+ ) -> str:
41
+ """
42
+ Generate a CSR (Certificate Signing Request) from an existing private key.
43
+ Returns:
44
+ str: The generated CSR in PEM format.
45
+ """
46
+
47
+ private_key = serialization.load_pem_private_key(
48
+ private_key_content,
49
+ password=None,
50
+ backend=default_backend(),
51
+ )
52
+
53
+ csr_builder = x509.CertificateSigningRequestBuilder()
54
+ csr_builder = csr_builder.subject_name(
55
+ x509.Name(
56
+ [
57
+ x509.NameAttribute(NameOID.COUNTRY_NAME, country),
58
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state),
59
+ x509.NameAttribute(NameOID.LOCALITY_NAME, locality),
60
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
61
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
62
+ ]
63
+ )).add_extension(
64
+ x509.BasicConstraints(ca=False, path_length=None),
65
+ critical=False,).add_extension(
66
+ x509.UnrecognizedExtension(
67
+ x509.ObjectIdentifier("2.16.840.1.113730.1.1"),
68
+ b"SSL Client, S/MIME",
69
+ ),
70
+ critical=False,).add_extension(
71
+ x509.UnrecognizedExtension(
72
+ x509.ObjectIdentifier("2.16.840.1.113730.1.13"),
73
+ b"Generated by OBS/OCD",
74
+ ),
75
+ critical=False,
76
+ ).add_extension(
77
+ x509.KeyUsage(
78
+ digital_signature=True,
79
+ crl_sign=False,
80
+ key_encipherment=True,
81
+ content_commitment=True,
82
+ data_encipherment=False,
83
+ key_agreement=False,
84
+ key_cert_sign=False,
85
+ encipher_only=False,
86
+ decipher_only=False,
87
+ ),
88
+ critical=True,
89
+ ).add_extension(
90
+ x509.ExtendedKeyUsage(
91
+ [
92
+ x509.ExtendedKeyUsageOID.CLIENT_AUTH,
93
+ x509.ExtendedKeyUsageOID.EMAIL_PROTECTION,
94
+ ]
95
+ ),
96
+ critical=False,
97
+ )
98
+
99
+ csr = csr_builder.sign(private_key, hashes.SHA256())
100
+ return csr.public_bytes(serialization.Encoding.PEM).decode("utf-8")
@@ -0,0 +1,3 @@
1
+ from .getmac import __version__, get_mac_address
2
+
3
+ __all__ = ["get_mac_address"]
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from __future__ import print_function
5
+
6
+ import argparse
7
+ import logging
8
+ import sys
9
+
10
+ from . import getmac
11
+
12
+
13
+ def main(): # type: () -> None
14
+ parser = argparse.ArgumentParser(
15
+ "getmac",
16
+ description="Get the MAC address of system network "
17
+ "interfaces or remote hosts on the LAN",
18
+ )
19
+ parser.add_argument(
20
+ "--version", action="version", version="getmac %s" % getmac.__version__
21
+ )
22
+ parser.add_argument(
23
+ "-v", "--verbose", action="store_true", help="Enable output messages"
24
+ )
25
+ parser.add_argument(
26
+ "-d",
27
+ "--debug",
28
+ action="count",
29
+ help="Enable debugging output. Add characters to "
30
+ "increase verbosity of output, e.g. '-dd'.",
31
+ )
32
+ parser.add_argument(
33
+ "-N",
34
+ "--no-net",
35
+ "--no-network-requests",
36
+ action="store_true",
37
+ dest="NO_NET",
38
+ help="Do not use arping or send a UDP packet to refresh the ARP table",
39
+ )
40
+ parser.add_argument(
41
+ "--override-port",
42
+ type=int,
43
+ metavar="PORT",
44
+ help="Override the default UDP port used to refresh the ARP table "
45
+ "if network requests are enabled and arping is unavailable",
46
+ )
47
+ parser.add_argument(
48
+ "--override-platform",
49
+ type=str,
50
+ default=None,
51
+ metavar="PLATFORM",
52
+ help="Override the platform detection with the given value "
53
+ "(e.g. 'linux', 'windows', 'freebsd', etc.'). "
54
+ "Any values returned by platform.system() are valid.",
55
+ )
56
+ parser.add_argument(
57
+ "--force-method",
58
+ type=str,
59
+ default=None,
60
+ metavar="METHOD",
61
+ help="Force a specific method to be used, e.g. 'IpNeighborShow'. "
62
+ "This will be used regardless of it's method type or platform "
63
+ "compatibility, and Method.test() will NOT be checked!",
64
+ )
65
+
66
+ group = parser.add_mutually_exclusive_group(required=False)
67
+ group.add_argument(
68
+ "-i",
69
+ "--interface",
70
+ type=str,
71
+ default=None,
72
+ help="Name of a network interface on the system",
73
+ )
74
+ group.add_argument(
75
+ "-4", "--ip", type=str, default=None, help="IPv4 address of a remote host"
76
+ )
77
+ group.add_argument(
78
+ "-6", "--ip6", type=str, default=None, help="IPv6 address of a remote host"
79
+ )
80
+ group.add_argument(
81
+ "-n", "--hostname", type=str, default=None, help="Hostname of a remote host"
82
+ )
83
+
84
+ args = parser.parse_args()
85
+
86
+ if args.debug or args.verbose:
87
+ logging.basicConfig(
88
+ format="%(levelname)-8s %(message)s", level=logging.DEBUG, stream=sys.stderr
89
+ )
90
+
91
+ if args.debug:
92
+ getmac.DEBUG = args.debug
93
+
94
+ if args.override_port:
95
+ port = int(args.override_port)
96
+ getmac.log.debug(
97
+ "Using UDP port %d (overriding the default port %d)", port, getmac.PORT
98
+ )
99
+ getmac.PORT = port
100
+
101
+ if args.override_platform:
102
+ getmac.OVERRIDE_PLATFORM = args.override_platform.strip().lower()
103
+
104
+ if args.force_method:
105
+ getmac.FORCE_METHOD = args.force_method.strip().lower()
106
+
107
+ mac = getmac.get_mac_address(
108
+ interface=args.interface,
109
+ ip=args.ip,
110
+ ip6=args.ip6,
111
+ hostname=args.hostname,
112
+ network_request=not args.NO_NET,
113
+ )
114
+
115
+ if mac is not None:
116
+ print(mac) # noqa: T001, T201
117
+ sys.exit(0) # Exit success!
118
+ else:
119
+ sys.exit(1) # Exit with error since it failed to find a MAC
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()