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.
- zscams/__init__.py +0 -0
- zscams/__main__.py +101 -0
- zscams/agent/__init__.py +0 -0
- zscams/agent/certificates/.gitkeep +0 -0
- zscams/agent/config.yaml +121 -0
- zscams/agent/keys/autoport.key +27 -0
- zscams/agent/src/__init__.py +0 -0
- zscams/agent/src/core/__init__.py +1 -0
- zscams/agent/src/core/api/backend/bootstrap.py +21 -0
- zscams/agent/src/core/api/backend/client.py +242 -0
- zscams/agent/src/core/api/backend/exceptions.py +10 -0
- zscams/agent/src/core/api/backend/update_machine_info.py +16 -0
- zscams/agent/src/core/prerequisites.py +36 -0
- zscams/agent/src/core/service_health_check.py +49 -0
- zscams/agent/src/core/services.py +86 -0
- zscams/agent/src/core/tunnel/__init__.py +144 -0
- zscams/agent/src/core/tunnel/tls.py +56 -0
- zscams/agent/src/core/tunnels.py +55 -0
- zscams/agent/src/services/__init__.py +0 -0
- zscams/agent/src/services/reverse_ssh.py +73 -0
- zscams/agent/src/services/ssh_forwarder.py +75 -0
- zscams/agent/src/services/system_monitor.py +264 -0
- zscams/agent/src/support/__init__.py +0 -0
- zscams/agent/src/support/configuration.py +53 -0
- zscams/agent/src/support/filesystem.py +41 -0
- zscams/agent/src/support/logger.py +40 -0
- zscams/agent/src/support/mac.py +18 -0
- zscams/agent/src/support/network.py +49 -0
- zscams/agent/src/support/openssl.py +100 -0
- zscams/libs/getmac/__init__.py +3 -0
- zscams/libs/getmac/__main__.py +123 -0
- zscams/libs/getmac/getmac.py +1900 -0
- zscams/libs/getmac/shutilwhich.py +67 -0
- zscams-2.0.1.dist-info/METADATA +118 -0
- zscams-2.0.1.dist-info/RECORD +37 -0
- zscams-2.0.1.dist-info/WHEEL +4 -0
- zscams-2.0.1.dist-info/entry_points.txt +3 -0
zscams/__init__.py
ADDED
|
File without changes
|
zscams/__main__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for ZSCAMs Agent
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import asyncio
|
|
7
|
+
import sys
|
|
8
|
+
import argparse
|
|
9
|
+
from zscams.agent.src.core.api.backend.bootstrap import bootstrap
|
|
10
|
+
from zscams.agent.src.core.api.backend.update_machine_info import update_machine_info
|
|
11
|
+
from zscams.agent.src.support.configuration import get_config, CONFIG_PATH
|
|
12
|
+
from zscams.agent.src.support.logger import get_logger
|
|
13
|
+
from zscams.agent.src.core.tunnel.tls import create_ssl_context
|
|
14
|
+
from zscams.agent.src.core.tunnels import start_all_tunnels
|
|
15
|
+
from zscams.agent.src.core.services import start_all_services
|
|
16
|
+
from zscams.agent.src.core.service_health_check import monitor_services
|
|
17
|
+
from zscams.agent.src.core.api.backend.client import backend_client
|
|
18
|
+
|
|
19
|
+
logger = get_logger("tls_tunnel_main")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ensure_bootstrapped():
|
|
23
|
+
"""Ensure the agent is bootstrapped with the backend."""
|
|
24
|
+
|
|
25
|
+
bootstrapped = backend_client.is_bootstrapped()
|
|
26
|
+
logger.debug("Agent bootstrapped: %s", bootstrapped)
|
|
27
|
+
if not bootstrapped:
|
|
28
|
+
logger.error(
|
|
29
|
+
"Agent is not bootstrapped. Please run `zscams --bootstrap` to start the bootstrap process.",
|
|
30
|
+
)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def async_main():
|
|
35
|
+
"""Asynchronous main function to start tunnels and services."""
|
|
36
|
+
|
|
37
|
+
config = get_config()
|
|
38
|
+
config_dir = os.path.dirname(CONFIG_PATH)
|
|
39
|
+
ssl_context = create_ssl_context(config["remote"], config_dir=config_dir)
|
|
40
|
+
remote_host = config["remote"]["host"]
|
|
41
|
+
remote_port = config["remote"]["port"]
|
|
42
|
+
|
|
43
|
+
# Start tunnels and wait for readiness
|
|
44
|
+
tunnel_tasks = await start_all_tunnels(
|
|
45
|
+
config.get("forwards", []), remote_host, remote_port, ssl_context
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Start services that depend on tunnels
|
|
49
|
+
service_tasks = asyncio.create_task(
|
|
50
|
+
start_all_services(config.get("services", []), config_dir=config_dir)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Start health checks
|
|
54
|
+
monitor_task = asyncio.create_task(
|
|
55
|
+
monitor_services(
|
|
56
|
+
config.get("services", []),
|
|
57
|
+
config_dir=config_dir,
|
|
58
|
+
interval=config.get("general", {}).get("health_check_interval", 300),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
logger.info("[*] All tunnels and services started. Press Ctrl+C to stop.")
|
|
63
|
+
await asyncio.gather(*tunnel_tasks, service_tasks, monitor_task)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
"""Main function to run the asynchronous main."""
|
|
68
|
+
parser = argparse.ArgumentParser(description="ZSCAMs Agent")
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--bootstrap",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Run bootstrap process and exit",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--update-machine-info",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Run bootstrap process and exit",
|
|
78
|
+
)
|
|
79
|
+
args = parser.parse_args()
|
|
80
|
+
|
|
81
|
+
if args.bootstrap:
|
|
82
|
+
try:
|
|
83
|
+
bootstrap()
|
|
84
|
+
sys.exit(0)
|
|
85
|
+
except Exception as exception:
|
|
86
|
+
logger.error(exception)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
if args.update_machine_info:
|
|
90
|
+
update_machine_info()
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
ensure_bootstrapped()
|
|
95
|
+
asyncio.run(async_main())
|
|
96
|
+
except KeyboardInterrupt:
|
|
97
|
+
logger.info("Exiting TLS Tunnel Client")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
main()
|
zscams/agent/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
zscams/agent/config.yaml
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
logging:
|
|
3
|
+
level: 10
|
|
4
|
+
|
|
5
|
+
general:
|
|
6
|
+
health_check_interval: 30 # seconds between health checks for services
|
|
7
|
+
|
|
8
|
+
remote:
|
|
9
|
+
host: 81.255.240.214 # stunnel server IP or hostname
|
|
10
|
+
port: 443 # Single TLS port on stunnel server
|
|
11
|
+
verify_cert: false # set false for self-signed certs
|
|
12
|
+
ca_cert: certificates/ca_chain.pem
|
|
13
|
+
client_cert: certificates/client.crt
|
|
14
|
+
client_key: certificates/client.key
|
|
15
|
+
ca_chain: certificates/ca_chain.pem
|
|
16
|
+
|
|
17
|
+
forwards:
|
|
18
|
+
- local_port: 4422
|
|
19
|
+
sni_hostname: ssh-tunnel
|
|
20
|
+
description: "Forward to SSH backend"
|
|
21
|
+
- local_port: 4423
|
|
22
|
+
sni_hostname: monitor-tunnel
|
|
23
|
+
description: "Forward to Seculogs"
|
|
24
|
+
|
|
25
|
+
backend:
|
|
26
|
+
base_url: "http://22.0.122.40:5000/api"
|
|
27
|
+
cache_dir: ".cache"
|
|
28
|
+
|
|
29
|
+
bootstrap_info:
|
|
30
|
+
csr:
|
|
31
|
+
country: "FR"
|
|
32
|
+
state: "Ile-de-France"
|
|
33
|
+
locality: "Paris"
|
|
34
|
+
common_name: "zscams-agent.example.com"
|
|
35
|
+
organization: "zscams-agent.orangecyberdefense.com"
|
|
36
|
+
cert_issuer: "ZSCAMs"
|
|
37
|
+
blacklist_ports: []
|
|
38
|
+
|
|
39
|
+
services:
|
|
40
|
+
- name: "Reverse SSH"
|
|
41
|
+
script: "src/services/reverse_ssh.py"
|
|
42
|
+
# Its just for reference in logs but not used within the service as the
|
|
43
|
+
# actual port is passed via params (Remote Port). Im setting it to 22 as
|
|
44
|
+
# its the eventual port that will be connected to
|
|
45
|
+
port: 22
|
|
46
|
+
args: []
|
|
47
|
+
health:
|
|
48
|
+
host: localhost
|
|
49
|
+
# port that should be open if service is healthy
|
|
50
|
+
port: 22
|
|
51
|
+
|
|
52
|
+
# -R [remote_listen_address:]remote_listen_port:local_host:local_port
|
|
53
|
+
params:
|
|
54
|
+
remote_host: "localhost"
|
|
55
|
+
# The forwarding port on ZSCAMs server to connect to in order to create
|
|
56
|
+
# the reverse tunnel, this should match the local_port of one of the
|
|
57
|
+
# forwards above
|
|
58
|
+
local_port: 4422
|
|
59
|
+
server_ssh_user: "ssh_user"
|
|
60
|
+
# Port on ZSCAMS server to forward to this agent, this port should be
|
|
61
|
+
# dynamically assigned by the server and passed to the agent via some
|
|
62
|
+
# secure means
|
|
63
|
+
reverse_port: 53612
|
|
64
|
+
# Path to private key for authentication
|
|
65
|
+
private_key: 'zscams/agent/keys/autoport.key'
|
|
66
|
+
ssh_options:
|
|
67
|
+
- "-o LogLevel=error"
|
|
68
|
+
- "-o UserKnownHostsFile=/dev/null"
|
|
69
|
+
- "-o StrictHostKeyChecking=no"
|
|
70
|
+
- "-o ServerAliveInterval=30"
|
|
71
|
+
- "-o ServerAliveCountMax=2"
|
|
72
|
+
prerequisites:
|
|
73
|
+
files:
|
|
74
|
+
- 'zscams/agent/keys/autoport.key'
|
|
75
|
+
services: []
|
|
76
|
+
ports:
|
|
77
|
+
- 22
|
|
78
|
+
- name: "Monitor Forwarder"
|
|
79
|
+
script: "src/services/ssh_forwarder.py"
|
|
80
|
+
port: 530
|
|
81
|
+
args: []
|
|
82
|
+
health:
|
|
83
|
+
host: localhost
|
|
84
|
+
# port that should be open if service is healthy
|
|
85
|
+
port: 44514
|
|
86
|
+
params:
|
|
87
|
+
remote_host: "194.51.14.159"
|
|
88
|
+
remote_port: 530
|
|
89
|
+
local_port: 4423 #Stunnel port
|
|
90
|
+
forwarder_port: 44514 #Port the forwarder will listen on and forward to remote host and port
|
|
91
|
+
server_ssh_user: "ssh_user"
|
|
92
|
+
private_key: 'zscams/agent/keys/autoport.key'
|
|
93
|
+
ssh_options:
|
|
94
|
+
- "-o LogLevel=error"
|
|
95
|
+
- "-o UserKnownHostsFile=/dev/null"
|
|
96
|
+
- "-o StrictHostKeyChecking=no"
|
|
97
|
+
- "-o ServerAliveInterval=30"
|
|
98
|
+
- "-o ServerAliveCountMax=2"
|
|
99
|
+
prerequisites:
|
|
100
|
+
files:
|
|
101
|
+
- 'zscams/agent/keys/autoport.key'
|
|
102
|
+
services: []
|
|
103
|
+
ports: []
|
|
104
|
+
- name: "Monitor Service"
|
|
105
|
+
script: "src/services/system_monitor.py"
|
|
106
|
+
port: 44514
|
|
107
|
+
args: []
|
|
108
|
+
health:
|
|
109
|
+
host: localhost
|
|
110
|
+
port: 44514
|
|
111
|
+
params:
|
|
112
|
+
remote_host: "localhost"
|
|
113
|
+
remote_port: 44514
|
|
114
|
+
equipment_name: "zpa-connector"
|
|
115
|
+
equipment_type: "zpa"
|
|
116
|
+
service_name: "Zscaler-AppConnector"
|
|
117
|
+
prerequisites:
|
|
118
|
+
files: []
|
|
119
|
+
services: []
|
|
120
|
+
ports:
|
|
121
|
+
- 44514
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
2
|
+
MIIEpQIBAAKCAQEA4kpci9i3k9OhuYpC1+0OSQCpXVRThIjtmwl9Nl4oRSsse9zj
|
|
3
|
+
vkzPShMvlZwvEzzZ/Y3hVDHLD5W3hhy63pyi75uxC9RDzb/7ckc1SdunTzj0mtAt
|
|
4
|
+
FhV+cXFZfv79NfaluN/WZwrbpMO+7+uATuEFIGe/t05PgYX9CzhaagCsC355jFHt
|
|
5
|
+
t2z/9Ul40mJkpMZwr+pk6BOSo/DeW7LNikFljqU8BjoZmGuwGuIHzF6ivhDA8hRz
|
|
6
|
+
aKVfw3e+BuxSyOH3xWxzpHSYiZGU5x9hHk8VEbkP7lISxqYdncCw1Is0O6awfXs0
|
|
7
|
+
azx7yzfUumjDdNSmrwpb5/ard30x/aE96upnRwIDAQABAoIBAQC2ghUcMWD2WDTS
|
|
8
|
+
iSGaNzZTSLZQcKefeDRy23didxRxnP2WtLP2EssIymqRdtM859JFPr4igrpsymqn
|
|
9
|
+
Ptq0mo6LQ/3KNZuuAQ4SwD3JYOAS9DPL/OSwMAu4ARyWYZ/lexV0AwxQNSCrRbjk
|
|
10
|
+
lgL5G2FgHm0wsXdMVr2c5Al//yTDauzgOJtQfi/LzuHusnz05iHzEp0ByBfiRL8z
|
|
11
|
+
xKQ9LFW7pIYUsgn4kXry5tQokH6NwDZ7vGPcjAQLD0lLDfAHF9HOiwpkQYXw8q91
|
|
12
|
+
lNvhaFHxa9jyfvuG9e66Q72YCevsCGBEl8Hvmbpz5/9oBZ6Pt82BZKjSouYh7Cya
|
|
13
|
+
a+JVxQcxAoGBAPSKdJdMXH6hCi5DBYhWnGSpDKvRwvxb2PgntCwajkaEZmuITj4y
|
|
14
|
+
dk9IH89ryl0yAmF6q2pKLyWHOir0fVX1HsnC9m2SErhMuSrhSVpcOLiI09wFwcAM
|
|
15
|
+
PRbS/6g1h/Q+MoaVuD0h9ElO8GkONBIBrmiVYqOifotXuPXjMMGT8oNpAoGBAOzk
|
|
16
|
+
+OSilwjwtwv/MkLOXqGFymSnG7l5zmt/KGxMCThjxY9b1DzyvXcUvojCRjFl4L0W
|
|
17
|
+
OyDbKRiEJZHSH6jV1gj2N8/53b3C1ziCRvHWvix7MJ7DeDfZLp0QEYZBm6Xm7mG+
|
|
18
|
+
TZ27kya0Hprev/t960UfOpwhb8+MNd1dxdgspC8vAoGBALcGpcrTyWqxZ2hGm252
|
|
19
|
+
vKkOacBzyAePSu448T4NRi17TRjwtPcSV8BxD/X0DEsCcgu5f3CXQ4BIHP4nbWOX
|
|
20
|
+
icqi1EQgD0jHi9OPOJKb8YwURNUpreDqiBJ8LAMexbnFj5Vxm6qNrkPsBD3s9oX/
|
|
21
|
+
oiT+ogwtQ59RMcs/lq9b5yf5AoGAbPmYFXVGDXLOgdJPiLPujFdDl7HX6ybBcmn4
|
|
22
|
+
ank/9JTRGPWhWLhBuDnuvHLCX48CJ3nGkYLAEOsZbU9ACSb1YwIBAsdq3hR3dSNZ
|
|
23
|
+
B39F1KiG4UICV46tBsuRhDVCKLtnBcfJZLoZI0DQo2W84zA1voJzL8eh69QQI1kz
|
|
24
|
+
3hILJTkCgYEAwT+XKnxX8jExNvpe71O2pm3mit6UyOHXB1KyT4Y3UuKjDxwZZ4w1
|
|
25
|
+
vPp6eQ3VcI+4+mpf0qs1cgDz8w35EInt60nKy0+0hwQaM7N0EmsguASHRprESZR9
|
|
26
|
+
dAPDJQ1UJlAMSJxZxFzTvMzDQHY+86GGh5mdoNwJDb0142etE7rSFUI=
|
|
27
|
+
-----END RSA PRIVATE KEY-----
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
running_services = set()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from zscams.agent.src.core.api.backend.client import backend_client
|
|
2
|
+
from zscams.agent.src.support.logger import get_logger
|
|
3
|
+
|
|
4
|
+
logger = get_logger("agent_bootstrap")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def bootstrap():
|
|
8
|
+
"""Ensure the agent is bootstrapped with the backend."""
|
|
9
|
+
|
|
10
|
+
if backend_client.is_bootstrapped():
|
|
11
|
+
logger.info("Agent ID %s is already bootstrapped.", backend_client.agent_id)
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
username = input("LDAP Username: ")
|
|
15
|
+
totp = input("TOTP: ")
|
|
16
|
+
|
|
17
|
+
backend_client.login(username, totp)
|
|
18
|
+
|
|
19
|
+
equipment_name = input("Equipment Name: ")
|
|
20
|
+
enforced_id = input("Enforced Agent ID (Optional): ")
|
|
21
|
+
backend_client.bootstrap(equipment_name, enforced_id or None)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from typing import Optional, cast
|
|
7
|
+
|
|
8
|
+
from .exceptions import AgentBootstrapError
|
|
9
|
+
|
|
10
|
+
from zscams.agent.src.support.configuration import CONFIG_PATH, ROOT_PATH, get_config
|
|
11
|
+
from zscams.agent.src.support.mac import get_mac_address
|
|
12
|
+
from zscams.agent.src.support.filesystem import resolve_path, ensure_dir, is_file_exists
|
|
13
|
+
from zscams.agent.src.support.network import get_local_hostname, get_local_ip_address
|
|
14
|
+
from zscams.agent.src.support.openssl import (
|
|
15
|
+
generate_csr_from_private_key,
|
|
16
|
+
generate_private_key,
|
|
17
|
+
)
|
|
18
|
+
from zscams.agent.src.support.logger import get_logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BackendClient:
|
|
22
|
+
"""Client to interact with the backend service."""
|
|
23
|
+
|
|
24
|
+
MACHINE_INFO_FILE_NAME = "machine_info.json"
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
config = get_config()
|
|
28
|
+
self.services_config = config.get("services", [])
|
|
29
|
+
self.backend_config = config.get("backend", {})
|
|
30
|
+
self.bootstrap_info = config.get("bootstrap_info", {})
|
|
31
|
+
self.remote_config = config.get("remote", {})
|
|
32
|
+
self.url = self.backend_config.get("base_url")
|
|
33
|
+
self.session = requests.session()
|
|
34
|
+
self.agent_id = get_mac_address()
|
|
35
|
+
self.logger = get_logger("backend_client")
|
|
36
|
+
|
|
37
|
+
self.__machine_cache_path = Path(
|
|
38
|
+
os.path.join(
|
|
39
|
+
ROOT_PATH.parent,
|
|
40
|
+
self.backend_config.get("cache_dir"),
|
|
41
|
+
self.MACHINE_INFO_FILE_NAME,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def bootstrap(self, equipment_name: str, enforced_id: Optional[str] = None):
|
|
46
|
+
"""Bootstrap the agent with the backend."""
|
|
47
|
+
|
|
48
|
+
if not self.is_authenticated():
|
|
49
|
+
self.logger.error("Couldn't authenticate")
|
|
50
|
+
raise AgentBootstrapError(
|
|
51
|
+
"Client is not authenticated to bootstrap. Please login first."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self.logger.info("bootstrapping agent ID %s", self.agent_id)
|
|
55
|
+
|
|
56
|
+
csr_info = self.bootstrap_info.get("csr", {})
|
|
57
|
+
remote_configs = get_config().get("remote", {})
|
|
58
|
+
|
|
59
|
+
private_key_path = cast(
|
|
60
|
+
str,
|
|
61
|
+
resolve_path(
|
|
62
|
+
remote_configs.get("client_key"), os.path.dirname(CONFIG_PATH)
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.logger.info("Generating the private key...")
|
|
67
|
+
private_key_content = generate_private_key(private_key_path)
|
|
68
|
+
|
|
69
|
+
self.logger.info("Generated the private key at %s", private_key_path)
|
|
70
|
+
|
|
71
|
+
ip = get_local_ip_address()
|
|
72
|
+
csr = generate_csr_from_private_key(
|
|
73
|
+
private_key_content,
|
|
74
|
+
csr_info.get("country"),
|
|
75
|
+
csr_info.get("state"),
|
|
76
|
+
csr_info.get("locality"),
|
|
77
|
+
csr_info.get("organization"),
|
|
78
|
+
csr_info.get("common_name"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
payload = {
|
|
82
|
+
"private_ip": ip,
|
|
83
|
+
"lan_ip": ip,
|
|
84
|
+
"hostname": get_local_hostname(),
|
|
85
|
+
"equipment_name": equipment_name,
|
|
86
|
+
"csr": csr,
|
|
87
|
+
"enforced_id": enforced_id,
|
|
88
|
+
"services": [service.get("name") for service in self.services_config],
|
|
89
|
+
"blacklist_ports": self.bootstrap_info.get("blacklist_ports", []),
|
|
90
|
+
"cert_issuer": self.bootstrap_info.get("cert_issuer", None),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
self.logger.debug(
|
|
94
|
+
"bootstrapping the agent with payload %s", json.dumps(payload, indent=2)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
res = self._post(f"agent-bootstrap/{self.agent_id}", payload)
|
|
98
|
+
res_json = res.json()
|
|
99
|
+
|
|
100
|
+
if not res.ok or res_json.get("success") is False:
|
|
101
|
+
message = (
|
|
102
|
+
res_json.get("message", None)
|
|
103
|
+
or res_json.get("error", None)
|
|
104
|
+
or res_json.get("errors", None)
|
|
105
|
+
or "Unknown error"
|
|
106
|
+
)
|
|
107
|
+
raise AgentBootstrapError(
|
|
108
|
+
f"Bootstrapping failed on backend request: {message}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.logger.info("Signed the cert")
|
|
112
|
+
self.logger.debug("Signed cert response: %s", json.dumps(res_json, indent=2))
|
|
113
|
+
|
|
114
|
+
ca_chain: list[str] = res_json.get("response", {}).get("ca_chain", [])
|
|
115
|
+
signed_cert: str = res_json.get("response", {}).get("signed_certificate", [])
|
|
116
|
+
self._write_certificates(ca_chain, signed_cert)
|
|
117
|
+
|
|
118
|
+
# To cache the machine info
|
|
119
|
+
self.get_machine_info(ignore_cache=True)
|
|
120
|
+
|
|
121
|
+
return res_json
|
|
122
|
+
|
|
123
|
+
def get_machine_info(self, ignore_cache: bool = False):
|
|
124
|
+
"""Get machine information from the backend."""
|
|
125
|
+
|
|
126
|
+
if not ignore_cache and is_file_exists(self.__machine_cache_path, self.logger):
|
|
127
|
+
return self.__load_machine_info_from_cache()
|
|
128
|
+
|
|
129
|
+
res = self._get(f"customers-machines/machine-id/{self.agent_id}")
|
|
130
|
+
res.raise_for_status()
|
|
131
|
+
|
|
132
|
+
machine_info = res.json().get("response", {})
|
|
133
|
+
self.logger.debug(
|
|
134
|
+
"Fetched machine info: %s", json.dumps(machine_info, indent=2)
|
|
135
|
+
)
|
|
136
|
+
self.__cache_machine_info(machine_info)
|
|
137
|
+
return machine_info
|
|
138
|
+
|
|
139
|
+
def __load_machine_info_from_cache(self):
|
|
140
|
+
"""Cache machine information to a local file."""
|
|
141
|
+
|
|
142
|
+
self.logger.info(
|
|
143
|
+
"Loading machine info from cache at %s", self.__machine_cache_path
|
|
144
|
+
)
|
|
145
|
+
with open(self.__machine_cache_path, "r", encoding="utf-8") as cache_file:
|
|
146
|
+
return json.load(cache_file)
|
|
147
|
+
|
|
148
|
+
def __cache_machine_info(self, machine_info: dict):
|
|
149
|
+
"""Cache machine information to a local file."""
|
|
150
|
+
|
|
151
|
+
ensure_dir(str(self.__machine_cache_path.parent.absolute()))
|
|
152
|
+
self.logger.info("Caching machine info to %s", self.__machine_cache_path)
|
|
153
|
+
with open(self.__machine_cache_path, "w", encoding="utf-8") as cache_file:
|
|
154
|
+
json.dump(machine_info, cache_file, indent=2)
|
|
155
|
+
|
|
156
|
+
def _write_certificates(self, ca_chain: list[str], cert: str):
|
|
157
|
+
if cert:
|
|
158
|
+
cert_path = os.path.join(ROOT_PATH, self.remote_config.get("client_cert"))
|
|
159
|
+
|
|
160
|
+
self.logger.info("Writing signed certificate to %s", cert_path)
|
|
161
|
+
with open(cert_path, "w", encoding="utf-8") as cert_file:
|
|
162
|
+
cert_file.write(cert)
|
|
163
|
+
|
|
164
|
+
if ca_chain:
|
|
165
|
+
ca_chain_path = os.path.join(ROOT_PATH, self.remote_config.get("ca_chain"))
|
|
166
|
+
|
|
167
|
+
self.logger.info("Writing CA chain to %s", ca_chain_path)
|
|
168
|
+
with open(ca_chain_path, "w", encoding="utf-8") as ca_chain_file:
|
|
169
|
+
ca_chain_file.write("\n\n".join(ca_chain))
|
|
170
|
+
|
|
171
|
+
def is_bootstrapped(self) -> bool:
|
|
172
|
+
"""Check if the agent is bootstrapped."""
|
|
173
|
+
|
|
174
|
+
is_bootstrapped_res = self._get(
|
|
175
|
+
f"agent-bootstrap/{self.agent_id}/is-bootstrapped"
|
|
176
|
+
)
|
|
177
|
+
is_bootstrapped_res.raise_for_status()
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
is_bootstrapped_res.json().get("response", {}).get("is-bootstrapped", False)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _get(self, path: str):
|
|
184
|
+
"""Perform a GET request to the backend."""
|
|
185
|
+
|
|
186
|
+
return self.session.get(self.__prepare_path(path))
|
|
187
|
+
|
|
188
|
+
def _post(self, path: str, body: dict):
|
|
189
|
+
"""Perform a POST request to the backend."""
|
|
190
|
+
|
|
191
|
+
return self.session.post(self.__prepare_path(path), json=body)
|
|
192
|
+
|
|
193
|
+
def _put(self, path: str, body: dict):
|
|
194
|
+
"""Perform a PUT request to the backend."""
|
|
195
|
+
|
|
196
|
+
return self.session.put(self.__prepare_path(path), json=body)
|
|
197
|
+
|
|
198
|
+
def _delete(self, path: str):
|
|
199
|
+
"""Perform a DELETE request to the backend."""
|
|
200
|
+
|
|
201
|
+
return self.session.delete(self.__prepare_path(path))
|
|
202
|
+
|
|
203
|
+
def login(self, username: str, totp: str):
|
|
204
|
+
"""Perform login to the backend using username and TOTP."""
|
|
205
|
+
|
|
206
|
+
payload = {
|
|
207
|
+
"username": username,
|
|
208
|
+
"totp": totp,
|
|
209
|
+
"client_id": "orchestrator-client",
|
|
210
|
+
"grant_type": "password",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
self.logger.info("Logging in to backend as %s", username)
|
|
214
|
+
|
|
215
|
+
response = self.session.post(
|
|
216
|
+
self.__prepare_path("authentication/temp/login"), json=payload
|
|
217
|
+
)
|
|
218
|
+
if not response.ok:
|
|
219
|
+
self.logger.error(
|
|
220
|
+
"Login failed with status code %d: %s",
|
|
221
|
+
response.status_code,
|
|
222
|
+
response.text,
|
|
223
|
+
)
|
|
224
|
+
response.raise_for_status()
|
|
225
|
+
|
|
226
|
+
token = response.json().get("access_token")
|
|
227
|
+
if token:
|
|
228
|
+
self.session.headers.update({"Authorization": f"Bearer {token}"})
|
|
229
|
+
|
|
230
|
+
self.logger.info("Login successful")
|
|
231
|
+
|
|
232
|
+
return response.json()
|
|
233
|
+
|
|
234
|
+
def is_authenticated(self) -> bool:
|
|
235
|
+
"""Check if the client is authenticated."""
|
|
236
|
+
return self.session.headers.get("Authorization") is not None
|
|
237
|
+
|
|
238
|
+
def __prepare_path(self, path):
|
|
239
|
+
return f"{self.url}/{path.lstrip('/')}"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
backend_client = BackendClient()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AgentBootstrapError(Exception):
|
|
2
|
+
"""Base exception for agent bootstrap-related errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AgentNotBootstrapped(AgentBootstrapError):
|
|
6
|
+
"""Raised when an operation requires a bootstrapped agent, but the agent is not bootstrapped."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentAlreadyBootstrapped(AgentBootstrapError):
|
|
10
|
+
"""Raised when attempting to bootstrap an already bootstrapped agent."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from zscams.agent.src.core.api.backend.client import backend_client
|
|
2
|
+
from zscams.agent.src.support.logger import get_logger
|
|
3
|
+
|
|
4
|
+
logger = get_logger("agent_machine_info")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def update_machine_info():
|
|
8
|
+
"""Ensure the agent is bootstrapped with the backend."""
|
|
9
|
+
|
|
10
|
+
username = input("LDAP Username: ")
|
|
11
|
+
totp = input("TOTP: ")
|
|
12
|
+
|
|
13
|
+
backend_client.login(username, totp)
|
|
14
|
+
|
|
15
|
+
logger.info("Getting machine info from backend...")
|
|
16
|
+
backend_client.get_machine_info(ignore_cache=True)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from zscams.agent.src.support.logger import get_logger
|
|
2
|
+
from zscams.agent.src.support.filesystem import is_file_exists
|
|
3
|
+
from zscams.agent.src.core import running_services
|
|
4
|
+
from zscams.agent.src.support.network import is_port_open, is_service_running
|
|
5
|
+
|
|
6
|
+
logger = get_logger("service_prerequisites")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_prerequisites(prereqs: dict) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Perform a one-shot prerequisites check (no waiting or retry).
|
|
12
|
+
Returns True only if all listed conditions are satisfied.
|
|
13
|
+
"""
|
|
14
|
+
if not prereqs:
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
results = []
|
|
18
|
+
|
|
19
|
+
# Check file prerequisites
|
|
20
|
+
for path in prereqs.get("files", []):
|
|
21
|
+
results.append(is_file_exists(path, logger))
|
|
22
|
+
|
|
23
|
+
# Check port prerequisites
|
|
24
|
+
for port in prereqs.get("ports", []):
|
|
25
|
+
results.append(is_port_open("localhost", port, logger))
|
|
26
|
+
|
|
27
|
+
# Check service prerequisites
|
|
28
|
+
for svc in prereqs.get("services", []):
|
|
29
|
+
results.append(is_service_running(svc, running_services, logger))
|
|
30
|
+
|
|
31
|
+
all_ok = all(results)
|
|
32
|
+
if not all_ok:
|
|
33
|
+
logger.warning("Some prerequisites failed for this service.")
|
|
34
|
+
else:
|
|
35
|
+
logger.info("All prerequisites satisfied.")
|
|
36
|
+
return all_ok
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health monitor for agent services.
|
|
3
|
+
|
|
4
|
+
This module periodically checks the defined health ports of all services.
|
|
5
|
+
If a service is not responding on its port, it will be restarted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from zscams.agent.src.support.logger import get_logger
|
|
10
|
+
from zscams.agent.src.core.services import start_service
|
|
11
|
+
from zscams.agent.src.core import running_services
|
|
12
|
+
from zscams.agent.src.support.network import is_port_open
|
|
13
|
+
|
|
14
|
+
logger = get_logger("health_monitor")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def monitor_services(services: list, config_dir: str, interval: int = 60):
|
|
18
|
+
"""
|
|
19
|
+
Periodically check service health by port.
|
|
20
|
+
|
|
21
|
+
If a service's health port is closed, attempt to restart it.
|
|
22
|
+
"""
|
|
23
|
+
logger.info(f"Health monitor started (interval={interval}s)")
|
|
24
|
+
await asyncio.sleep(30) # Initial delay to allow services to start
|
|
25
|
+
while True:
|
|
26
|
+
for svc in services:
|
|
27
|
+
name = svc["name"]
|
|
28
|
+
health = svc.get("health", {})
|
|
29
|
+
|
|
30
|
+
host = health.get("host", "localhost")
|
|
31
|
+
port = health.get("port")
|
|
32
|
+
if not port:
|
|
33
|
+
logger.debug(f"Skipping {name}: no health port defined.")
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
if name in running_services and is_port_open(host, port, logger):
|
|
37
|
+
logger.debug(f"{name} is healthy on {host}:{port}")
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
logger.warning(f"{name} not healthy, restarting...")
|
|
41
|
+
if name in running_services:
|
|
42
|
+
running_services.remove(name)
|
|
43
|
+
try:
|
|
44
|
+
await start_service(svc, config_dir)
|
|
45
|
+
logger.info(f"Restarted service: {name}")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Failed to restart {name}: {e}")
|
|
48
|
+
|
|
49
|
+
await asyncio.sleep(interval)
|