Skip to content

Commit 6929f3a

Browse files
Addressing feedback
1 parent 278152a commit 6929f3a

File tree

3 files changed

+140
-145
lines changed

3 files changed

+140
-145
lines changed

scripts/aws/ec2.py

Lines changed: 127 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -10,170 +10,197 @@
1010
import signal
1111
import argparse
1212
from botocore.exceptions import ClientError
13+
from typing import Dict
1314
import sys
1415
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15-
from confidential_compute import ConfidentialCompute
16+
from confidential_compute import ConfidentialCompute, OperatorConfig
17+
1618

1719
class EC2(ConfidentialCompute):
1820

1921
def __init__(self):
2022
super().__init__()
21-
self.config = {}
23+
self.configs: OperatorConfig = {}
2224

23-
def __get_aws_token(self):
25+
def __get_aws_token(self) -> str:
26+
"""Fetches a temporary AWS EC2 metadata token."""
2427
try:
2528
token_url = "http://169.254.169.254/latest/api/token"
26-
token_response = requests.put(token_url, headers={"X-aws-ec2-metadata-token-ttl-seconds": "3600"}, timeout=2)
27-
return token_response.text
28-
except Exception as e:
29-
return "blank"
30-
31-
def __get_current_region(self):
29+
response = requests.put(
30+
token_url, headers={"X-aws-ec2-metadata-token-ttl-seconds": "3600"}, timeout=2
31+
)
32+
return response.text
33+
except requests.RequestException as e:
34+
raise RuntimeError(f"Failed to fetch aws token: {e}")
35+
36+
def __get_current_region(self) -> str:
37+
"""Fetches the current AWS region from EC2 instance metadata."""
3238
token = self.__get_aws_token()
3339
metadata_url = "http://169.254.169.254/latest/dynamic/instance-identity/document"
3440
headers = {"X-aws-ec2-metadata-token": token}
3541
try:
36-
response = requests.get(metadata_url, headers=headers,timeout=2)
37-
if response.status_code == 200:
38-
return response.json().get("region")
39-
else:
40-
print(f"Failed to fetch region, status code: {response.status_code}")
41-
except Exception as e:
42-
raise Exception(f"Region not found, are you running in EC2 environment. {e}")
42+
response = requests.get(metadata_url, headers=headers, timeout=2)
43+
response.raise_for_status()
44+
return response.json()["region"]
45+
except requests.RequestException as e:
46+
raise RuntimeError(f"Failed to fetch region: {e}")
4347

44-
def _get_secret(self, secret_identifier):
45-
client = boto3.client("secretsmanager", region_name=self.__get_current_region())
48+
def _get_secret(self, secret_identifier: str) -> Dict:
49+
"""Fetches a secret value from AWS Secrets Manager."""
50+
region = self.__get_current_region()
51+
client = boto3.client("secretsmanager", region_name=region)
4652
try:
4753
secret = client.get_secret_value(SecretId=secret_identifier)
4854
return json.loads(secret["SecretString"])
4955
except ClientError as e:
50-
raise Exception("Unable to access secret store")
51-
52-
def __add_defaults(self, configs):
56+
raise RuntimeError(f"Unable to access Secrets Manager: {e}")
57+
58+
@staticmethod
59+
def __add_defaults(configs: Dict[str, any]) -> OperatorConfig:
60+
"""Adds default values to configuration if missing."""
5361
configs.setdefault("enclave_memory_mb", 24576)
5462
configs.setdefault("enclave_cpu_count", 6)
5563
configs.setdefault("debug_mode", False)
5664
return configs
5765

58-
def __setup_vsockproxy(self, log_level):
59-
thread_count = int((multiprocessing.cpu_count() + 1) // 2)
60-
log_level = log_level
66+
@staticmethod
67+
def __error_out_on_execute(command: list, error_message: str) -> None:
68+
"""Runs a command in the background and handles exceptions."""
6169
try:
62-
subprocess.Popen(["/usr/bin/vsockpx", "-c", "/etc/uid2operator/proxy.yaml", "--workers", str(thread_count), "--log-level", log_level, "--daemon"])
63-
print("VSOCK proxy is now running in the background")
64-
except FileNotFoundError:
65-
print("Error: vsockpx not found. Please ensure the path is correct")
70+
subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
6671
except Exception as e:
67-
print("Failed to start VSOCK proxy")
72+
print(f"{error_message} \n '{' '.join(command)}': {e}")
6873

69-
def __run_config_server(self, log_level):
74+
def __setup_vsockproxy(self, log_level: int) -> None:
75+
"""Sets up the vsock proxy service."""
76+
thread_count = (multiprocessing.cpu_count() + 1) // 2
77+
command = [
78+
"/usr/bin/vsockpx", "-c", "/etc/uid2operator/proxy.yaml",
79+
"--workers", str(thread_count), "--log-level", str(log_level), "--daemon"
80+
]
81+
self.__error_out_on_execute(command, "vsockpx not found. Ensure it is installed.")
82+
83+
def __run_config_server(self) -> None:
84+
"""Starts the Flask configuration server."""
7085
os.makedirs("/etc/secret/secret-value", exist_ok=True)
71-
with open('/etc/secret/secret-value/config', 'w') as fp:
72-
json.dump(self.configs, fp)
86+
config_path = "/etc/secret/secret-value/config"
87+
with open(config_path, 'w') as config_file:
88+
json.dump(self.configs, config_file)
7389
os.chdir("/opt/uid2operator/config-server")
74-
# TODO: Add --log-level to flask.
75-
try:
76-
subprocess.Popen(["./bin/flask", "run", "--host", "127.0.0.1", "--port", "27015"])
77-
print("Config server is now running in the background.")
78-
except Exception as e:
79-
print(f"Failed to start config server: {e}")
90+
command = ["./bin/flask", "run", "--host", "127.0.0.1", "--port", "27015"]
91+
self.__error_out_on_execute(command, "Failed to start the Flask config server.")
8092

81-
def __run_socks_proxy(self, log_level):
82-
subprocess.Popen(["sockd", "-d"])
93+
def __run_socks_proxy(self) -> None:
94+
"""Starts the SOCKS proxy service."""
95+
command = ["sockd", "-d"]
96+
self.__error_out_on_execute(command, "Failed to start socks proxy.")
8397

84-
def __get_secret_name_from_userdata(self):
98+
def __get_secret_name_from_userdata(self) -> str:
99+
"""Extracts the secret name from EC2 user data."""
85100
token = self.__get_aws_token()
86101
user_data_url = "http://169.254.169.254/latest/user-data"
87-
user_data_response = requests.get(user_data_url, headers={"X-aws-ec2-metadata-token": token})
88-
user_data = user_data_response.text
89-
identity_scope = open("/opt/uid2operator/identity_scope.txt").read().strip()
90-
default_name = "{}-operator-config-key".format(identity_scope.lower())
91-
hardcoded_value = "{}_CONFIG_SECRET_KEY".format(identity_scope.upper())
102+
response = requests.get(user_data_url, headers={"X-aws-ec2-metadata-token": token})
103+
user_data = response.text
104+
105+
with open("/opt/uid2operator/identity_scope.txt") as file:
106+
identity_scope = file.read().strip()
107+
108+
default_name = f"{identity_scope.lower()}-operator-config-key"
109+
hardcoded_value = f"{identity_scope.upper()}_CONFIG_SECRET_KEY"
92110
match = re.search(rf'^export {hardcoded_value}="(.+?)"$', user_data, re.MULTILINE)
93111
return match.group(1) if match else default_name
94112

95-
def _setup_auxilaries(self):
113+
def _setup_auxiliaries(self) -> None:
114+
"""Sets up the necessary auxiliary services and configurations."""
96115
hostname = os.getenv("HOSTNAME", default=os.uname()[1])
97-
file_path = "HOSTNAME"
98116
try:
99-
with open(file_path, "w") as file:
117+
with open("HOSTNAME", "w") as file:
100118
file.write(hostname)
101-
print(f"Hostname '{hostname}' written to {file_path}")
119+
print(f"Hostname '{hostname}' written to file.")
102120
except Exception as e:
103-
print(f"An error occurred : {e}")
121+
"""
122+
Ignoring error here, as we are currently not using this information anywhere.
123+
But can be added in future for tracibility on debug
124+
"""
125+
print(f"Error writing hostname: {e}")
126+
104127
config = self._get_secret(self.__get_secret_name_from_userdata())
105128
self.configs = self.__add_defaults(config)
106-
log_level = 3 if self.configs['debug_mode'] else 1
129+
log_level = 3 if self.configs["debug_mode"] else 1
107130
self.__setup_vsockproxy(log_level)
108-
self.__run_config_server(log_level)
109-
self.__run_socks_proxy(log_level)
131+
self.__run_config_server()
132+
self.__run_socks_proxy()
110133

111-
112-
def _validate_auxilaries(self):
134+
def _validate_auxiliaries(self) -> None:
135+
"""Validates auxiliary services."""
113136
proxy = "socks5h://127.0.0.1:3305"
114-
url = "http://127.0.0.1:27015/getConfig"
115-
response = requests.get(url)
116-
if response.status_code != 200:
117-
raise Exception("Config server unreachable")
118-
proxies = {
119-
"http": proxy,
120-
"https": proxy,
121-
}
137+
config_url = "http://127.0.0.1:27015/getConfig"
122138
try:
123-
response = requests.get(url, proxies=proxies)
124-
response.raise_for_status()
125-
except Exception as e:
126-
raise Exception(f"Cannot conect to config server through socks5: {e}")
139+
response = requests.get(config_url)
140+
response.raise_for_status()
141+
except requests.RequestException as e:
142+
raise RuntimeError(f"Config server unreachable: {e}")
143+
proxies = {"http": proxy, "https": proxy}
144+
try:
145+
response = requests.get(config_url, proxies=proxies)
146+
response.raise_for_status()
147+
except requests.RequestException as e:
148+
raise RuntimeError(f"Cannot connect to config server via SOCKS proxy: {e}")
127149

128-
def run_compute(self):
129-
self._setup_auxilaries()
130-
self._validate_auxilaries()
150+
def run_compute(self) -> None:
151+
"""Main execution flow for confidential compute."""
152+
self._setup_auxiliaries()
153+
self._validate_auxiliaries()
154+
self.validate_connectivity(self.configs)
131155
command = [
132156
"nitro-cli", "run-enclave",
133157
"--eif-path", "/opt/uid2operator/uid2operator.eif",
134-
"--memory", self.config['enclave_memory_mb'],
135-
"--cpu-count", self.config['enclave_cpu_count'],
136-
"--enclave-cid", 42,
158+
"--memory", str(self.configs["enclave_memory_mb"]),
159+
"--cpu-count", str(self.configs["enclave_cpu_count"]),
160+
"--enclave-cid", "42",
137161
"--enclave-name", "uid2operator"
138162
]
139-
if self.config['debug']:
140-
command+=["--debug-mode", "--attach-console"]
163+
if self.configs["debug_mode"]:
164+
command += ["--debug-mode", "--attach-console"]
141165
subprocess.run(command, check=True)
142166

143-
def cleanup(self):
144-
describe_output = subprocess.check_output(["nitro-cli", "describe-enclaves"], text=True)
145-
enclaves = json.loads(describe_output)
146-
enclave_id = enclaves[0].get("EnclaveID") if enclaves else None
147-
if enclave_id:
148-
subprocess.run(["nitro-cli", "terminate-enclave", "--enclave-id", enclave_id])
149-
print(f"Enclave with ID {enclave_id} has been terminated.")
150-
else:
151-
print("No enclave found or EnclaveID is null.")
152-
153-
def kill_process(self, process_name):
167+
def cleanup(self) -> None:
168+
"""Terminates the Nitro Enclave and auxiliary processes."""
154169
try:
155-
result = subprocess.run(
156-
["pgrep", "-f", process_name],
157-
stdout=subprocess.PIPE,
158-
text=True,
159-
check=False
160-
)
170+
describe_output = subprocess.check_output(["nitro-cli", "describe-enclaves"], text=True)
171+
enclaves = json.loads(describe_output)
172+
enclave_id = enclaves[0].get("EnclaveID") if enclaves else None
173+
if enclave_id:
174+
subprocess.run(["nitro-cli", "terminate-enclave", "--enclave-id", enclave_id])
175+
print(f"Terminated enclave with ID: {enclave_id}")
176+
else:
177+
print("No active enclaves found.")
178+
except subprocess.SubprocessError as e:
179+
raise (f"Error during cleanup: {e}")
180+
181+
def kill_process(self, process_name: str) -> None:
182+
"""Kills a process by its name."""
183+
try:
184+
result = subprocess.run(["pgrep", "-f", process_name], stdout=subprocess.PIPE, text=True, check=False)
161185
if result.stdout.strip():
162186
for pid in result.stdout.strip().split("\n"):
163187
os.kill(int(pid), signal.SIGKILL)
164-
print(f"{process_name} exited")
188+
print(f"Killed process '{process_name}'.")
165189
else:
166-
print(f"Process {process_name} not found")
190+
print(f"No process named '{process_name}' found.")
167191
except Exception as e:
168-
print(f"Failed to shut down {process_name}: {e}")
192+
print(f"Error killing process '{process_name}': {e}")
193+
169194

170195
if __name__ == "__main__":
171-
parser = argparse.ArgumentParser()
172-
parser.add_argument("-o", "--operation", required=False)
196+
parser = argparse.ArgumentParser(description="Manage EC2-based confidential compute workflows.")
197+
parser.add_argument("-o", "--operation", choices=["stop", "start"], default="start", help="Operation to perform.")
173198
args = parser.parse_args()
174199
ec2 = EC2()
175-
if args.operation and args.operation == "stop":
200+
if args.operation == "stop":
176201
ec2.cleanup()
177-
[ec2.kill_process(process) for process in ["vsockpx", "sockd", "vsock-proxy", "nohup"]]
202+
for process in ["vsockpx", "sockd", "vsock-proxy"]:
203+
ec2.kill_process(process)
178204
else:
179205
ec2.run_compute()
206+

scripts/aws/load_config.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

scripts/confidential_compute.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
import socket
44
from urllib.parse import urlparse
55
from abc import ABC, abstractmethod
6+
from typing import TypedDict, Dict
7+
8+
class OperatorConfig(TypedDict):
9+
enclave_memory_mb: int
10+
enclave_cpu_count: int
11+
debug_mode: bool
12+
api_token: str
13+
core_base_url: str
14+
optout_base_url: str
615

716
class ConfidentialCompute(ABC):
817

@@ -16,7 +25,7 @@ def _get_secret(self, secret_identifier):
1625
"""
1726
pass
1827

19-
def validate_operator_key(self, secrets):
28+
def validate_operator_key(self, secrets: OperatorConfig):
2029
"""
2130
Validates operator key if following new pattern. Ignores otherwise
2231
"""
@@ -31,7 +40,7 @@ def validate_operator_key(self, secrets):
3140
raise Exception("Operator key does not match the environment")
3241
return True
3342

34-
def validate_connectivity(self, config):
43+
def validate_connectivity(self, config: OperatorConfig):
3544
"""
3645
Validates core/optout is accessible.
3746
"""
@@ -52,14 +61,14 @@ def validate_connectivity(self, config):
5261
return
5362

5463
@abstractmethod
55-
def _setup_auxilaries(self, secrets):
64+
def _setup_auxiliaries(self):
5665
"""
5766
Sets up auxilary processes required to confidential compute
5867
"""
5968
pass
6069

6170
@abstractmethod
62-
def _validate_auxilaries(self, secrets):
71+
def _validate_auxiliaries(self):
6372
"""
6473
Validates auxilary services are running
6574
"""

0 commit comments

Comments
 (0)