1010import signal
1111import argparse
1212from botocore .exceptions import ClientError
13+ from typing import Dict
1314import sys
1415sys .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
1719class 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
170195if __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+
0 commit comments