Skip to content

Commit 531101a

Browse files
committed
Issue #5 - Replace JSON with SQLite
1 parent 4a92a24 commit 531101a

16 files changed

Lines changed: 523 additions & 30449 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
**Release Date: November 20th, 2024**
1313

14-
cloudexit is an open-source tool that empowers cloud engineers to conduct comprehensive cloud exit assessments. It helps identify and evaluate the risks associated with their cloud environment while providing actionable insights into the challenges and constraints of transitioning away from their current cloud provider. By leveraging EscapeCloud OSS, organizations can better prepare for a potential cloud exit, ensuring a smoother and more informed decision-making process.
14+
cloudexit is an open-source tool that empowers cloud engineers to conduct comprehensive cloud exit assessments. It helps identify and evaluate the risks associated with their cloud environment while providing actionable insights into the challenges and constraints of transitioning away from their current cloud provider. By leveraging EscapeCloud Community Edition, organizations can better prepare for a potential cloud exit, ensuring a smoother and more informed decision-making process.
15+
1516

1617
## Required Packages
1718

assets/template/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
77
<!-- Favicon & Title -->
88
<link rel="icon" type="image/png" sizes="16x16" href="assets/img/logo/favicon.png" />
9-
<title>EscapeCloud OSS - Cloud Exit Assessment Report</title>
9+
<title>EscapeCloud Community Edition - Cloud Exit Assessment Report</title>
1010
<!-- CSS only -->
1111
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
1212
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
@@ -24,7 +24,7 @@
2424
<div class="top-head">
2525
<h4>
2626
<img src="assets/img/logo/logo.png" width="30" alt="EscapeCloud" />
27-
EscapeCloud OSS - Cloud Exit Assessment Report
27+
EscapeCloud Community Edition - Cloud Exit Assessment Report
2828
</h4>
2929
</div>
3030
</header>

core/engine.py

Lines changed: 93 additions & 153 deletions
Large diffs are not rendered by default.

core/utils.py

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import shutil
44
import logging
55
from datetime import datetime
6+
from collections import defaultdict
7+
8+
logger = logging.getLogger("core.engine.utils")
69

710
def copy_assets(report_path):
811
assets_folders = ["css", "img", "icons"]
@@ -19,6 +22,18 @@ def copy_assets(report_path):
1922
if not os.path.exists(dest_path):
2023
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
2124

25+
# Copy datasets/data.db to data/assessment.db
26+
db_src_path = "datasets/data.db"
27+
db_dest_dir = os.path.join(report_path, "data")
28+
db_dest_path = os.path.join(db_dest_dir, "assessment.db")
29+
30+
# Create the 'data' directory if it doesn't exist
31+
os.makedirs(db_dest_dir, exist_ok=True)
32+
33+
# Only copy if the destination doesn't already exist
34+
if not os.path.exists(db_dest_path):
35+
shutil.copyfile(db_src_path, db_dest_path)
36+
2237

2338
def get_cost_summary(cost_data):
2439
months = []
@@ -32,6 +47,13 @@ def get_cost_summary(cost_data):
3247
"EUR": "€"
3348
}
3449

50+
# Convert list to dictionary if necessary
51+
if isinstance(cost_data, list):
52+
cost_data = {
53+
item["month"]: {"cost": item["cost"], "currency": item["currency"]}
54+
for item in cost_data
55+
}
56+
3557
# Extract currency from the first entry, assuming all costs use the same currency
3658
first_entry = next(iter(cost_data.values()), None)
3759
currency_code = first_entry.get("currency", "USD") if first_entry else "USD"
@@ -47,75 +69,72 @@ def get_cost_summary(cost_data):
4769
return months, cost_values, total_cost, currency_symbol
4870

4971
def get_risk_summary(risk_data, risk_definitions, resource_inventory):
50-
logger = logging.getLogger(__name__)
51-
5272
severity_order = {'high': 1, 'medium': 2, 'low': 3}
5373
severity_counts = {'high': 0, 'medium': 0, 'low': 0}
5474
sorted_risks = []
5575

5676
# Map resource IDs to resource names for quick lookup
57-
resource_name_map = {str(item['resource_type']): item['resource_name'] for item in resource_inventory.values()}
58-
59-
# Log the resource_name_map to verify it has been built correctly
60-
#logger.info(f"Resource Name Map: {resource_name_map}")
77+
resource_name_map = {str(key): value['name'] for key, value in resource_inventory.items()}
6178

62-
# Log the risk_data to verify its structure
63-
#logger.info(f"Risk Data: {risk_data}")
64-
65-
# Group risks by their risk code and track impacted resources
66-
risk_map = {}
79+
# Group risks by their risk code and impacted resources
80+
risk_map = defaultdict(lambda: {"impacted_resources": set(), "count": 0})
6781
for risk_entry in risk_data:
6882
risk_code = risk_entry['risk']
69-
resource_type = str(risk_entry['resource_type']) # Convert to string to match the map keys
70-
71-
# Initialize risk entry in the map if it doesn't exist
72-
if risk_code not in risk_map:
73-
risk_map[risk_code] = {
74-
"impacted_resources": set(), # To store unique resources
75-
"count": 0
76-
}
83+
resource_type = str(risk_entry['resource_type']) if risk_entry['resource_type'] != "null" else None
7784

78-
# If resource_type is not "null", add it to impacted resources
79-
if resource_type != "null":
85+
if resource_type:
86+
# Handle risks with associated resource types
8087
resource_name = resource_name_map.get(resource_type, "Unknown Resource")
8188
risk_map[risk_code]["impacted_resources"].add(resource_name)
8289
risk_map[risk_code]["count"] += 1
8390
else:
84-
# Mark this entry as a general risk without specific resources
91+
# Handle overall risks with no specific resource type
8592
risk_map[risk_code]["impacted_resources"] = []
8693
risk_map[risk_code]["count"] = None
8794

88-
# Log the intermediate risk_map to verify resource processing
89-
#logger.info(f"Risk Map After Processing: {risk_map}")
90-
91-
# Process each risk code in the map to populate sorted_risks
95+
# Process risk definitions
9296
for risk_code, risk_info in risk_map.items():
93-
# Look up the risk definition from risk_definitions
9497
risk_definition = next((rd for rd in risk_definitions if rd["id"] == risk_code), None)
9598
if not risk_definition:
9699
continue
97100

98101
severity = risk_definition['severity']
99102
severity_counts[severity] += 1
100103

101-
# Format the impacted resources and count
102-
impacted_resources = list(risk_info["impacted_resources"]) if risk_info["impacted_resources"] else []
103-
impacted_resources_count = risk_info["count"]
104-
105-
# Append detailed risk information
106104
sorted_risks.append({
107105
'name': risk_definition['name'],
108106
'description': risk_definition['description'],
109-
'impacted_resources': impacted_resources,
110-
'impacted_resources_count': impacted_resources_count,
107+
'impacted_resources': list(risk_info["impacted_resources"]),
108+
'impacted_resources_count': risk_info["count"],
111109
'severity': severity
112110
})
113111

114-
# Sort risks by severity level
112+
# Sort risks by severity
115113
sorted_risks.sort(key=lambda x: severity_order.get(x['severity'], 4))
116114

117-
# Log the final sorted risks for verification
118-
#logger.info(f"Sorted Risks: {sorted_risks}")
119-
#logger.info(f"Severity Counts: {severity_counts}")
120-
121115
return sorted_risks, severity_counts
116+
117+
def prepare_alternative_technologies(resource_inventory, alternatives, alternative_technologies, exit_strategy):
118+
alt_tech_data = []
119+
for resource in resource_inventory:
120+
resource_type = resource.get("resource_type")
121+
relevant_alternatives = [
122+
alt for alt in alternatives
123+
if str(alt["resource_type"]) == str(resource_type) and str(alt["strategy_type"]) == str(exit_strategy)
124+
]
125+
for alt in relevant_alternatives:
126+
tech = next(
127+
(t for t in alternative_technologies if t["id"] == alt["alternative_technology"] and t["status"] == "t"),
128+
None
129+
)
130+
if tech:
131+
alt_tech_data.append({
132+
"resource_type_id": resource_type,
133+
"product_name": tech.get("product_name"),
134+
"product_description": tech.get("product_description"),
135+
"product_url": tech.get("product_url"),
136+
"open_source": tech.get("open_source") == "t",
137+
"support_plan": tech.get("support_plan") == "t",
138+
"status": tech.get("status") == "t",
139+
})
140+
return alt_tech_data

core/utils_aws.py

Lines changed: 92 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
import time
77
import logging
88
from datetime import date, datetime, timedelta
9+
from collections import defaultdict
910
from dateutil.relativedelta import relativedelta
1011
from botocore.exceptions import NoCredentialsError, ClientError
1112

13+
from .utils_db import connect, load_data
14+
1215
logger = logging.getLogger("core.engine.aws")
1316

1417
def aws_api_call_with_retry(client, function_name, parameters, max_retries, retry_delay):
@@ -59,19 +62,20 @@ def build_aws_resource_inventory(cloud_service_provider, provider_details, repor
5962
region_name=region
6063
)
6164

62-
# Load the ResourceType mapping to include both `id` and `name`
63-
with open("datasets/resourcetype.json", "r", encoding="utf-8") as f:
64-
resource_type_mapping = {
65-
item["code"]: {"id": item["id"], "name": item["name"]}
66-
for item in json.load(f)
67-
if item["csp"] == "2" and item["status"] == "t"
68-
}
65+
db_path = os.path.join(report_path, "data", "assessment.db")
66+
67+
# Load the ResourceType mapping
68+
resource_type_mapping = {
69+
item["code"]: {"id": item["id"], "name": item["name"]}
70+
for item in load_data("resourcetype")
71+
if item["csp"] == 2 and item["status"] == "t"
72+
}
6973

70-
resource_summary = {}
74+
# Save raw data for debugging and auditing purposes
7175
raw_data = []
7276

73-
# Initialize a custom counter
74-
resource_inventory_id_counter = 1
77+
# Aggregate resources by type and location
78+
aggregated_resources = defaultdict(int)
7579

7680
# Iterate through each resource type in the JSON
7781
for idx, (resource_type_code, resource_info) in enumerate(resource_type_mapping.items(), start=1):
@@ -108,17 +112,9 @@ def build_aws_resource_inventory(cloud_service_provider, provider_details, repor
108112
#logger.warning(f"No valid response found for {service_name} operation {operation_name}. Skipping.")
109113
continue
110114

111-
# Count resources and add to summary if count > 0
112-
resource_count = len(resources)
113-
if resource_count > 0:
114-
resource_inventory_id = str(resource_inventory_id_counter)
115-
resource_summary[resource_inventory_id] = {
116-
"resource_name": resource_info["name"],
117-
"resource_type": resource_info["id"],
118-
"location": region,
119-
"count": resource_count
120-
}
121-
resource_inventory_id_counter += 1
115+
# Aggregate the resources
116+
for resource in resources:
117+
aggregated_resources[(resource_type_code, region)] += 1
122118

123119
# Store raw data
124120
raw_data.append({
@@ -127,9 +123,6 @@ def build_aws_resource_inventory(cloud_service_provider, provider_details, repor
127123
"resources": resources
128124
})
129125

130-
131-
#logger.info(f"Processed {resource_count} resources for service {service_name} with operation {operation_name}")
132-
133126
except (NoCredentialsError, ClientError, Exception) as e:
134127
#logger.error(f"Error while processing {service_name}: {str(e)}", exc_info=True)
135128
continue
@@ -140,13 +133,35 @@ def build_aws_resource_inventory(cloud_service_provider, provider_details, repor
140133
raw_file_path = os.path.join(raw_data_path, "resource_inventory_raw_data.json")
141134
with open(raw_file_path, "w", encoding="utf-8") as raw_file:
142135
json.dump(raw_data, raw_file, indent=4)
143-
#logger.info(f"AWS raw resource inventory saved to {raw_file_path}")
144136

145-
# Save structured data to a JSON file
146-
structured_file_path = os.path.join(report_path, "resource_inventory_standard_data.json")
147-
with open(structured_file_path, "w", encoding="utf-8") as structured_file:
148-
json.dump(resource_summary, structured_file, indent=4)
149-
#logger.info(f"AWS structured resource inventory saved to {structured_file_path}")
137+
# Insert aggregated data into SQLite
138+
with connect(db_path=db_path) as conn:
139+
cursor = conn.cursor()
140+
141+
for (resource_type_code, resource_location), resource_count in aggregated_resources.items():
142+
try:
143+
# Map resource type code to resource_type_id
144+
resource_info = resource_type_mapping.get(resource_type_code)
145+
if not resource_info:
146+
#logger.warning(f"Resource type {resource_type_code} not found in resourcetype mapping. Skipping.")
147+
continue
148+
149+
resource_type_id = resource_info["id"]
150+
151+
cursor.execute(
152+
"""
153+
INSERT INTO resource_inventory (resource_type, location, count)
154+
VALUES (?, ?, ?)
155+
ON CONFLICT(resource_type, location) DO UPDATE SET count = excluded.count
156+
""",
157+
(resource_type_id, resource_location, resource_count)
158+
)
159+
except sqlite3.Error as e:
160+
logger.error(f"SQLite error while processing aggregated resource: {e}", exc_info=True)
161+
except Exception as e:
162+
logger.error(f"Unexpected error while processing aggregated resource: {e}", exc_info=True)
163+
164+
conn.commit()
150165

151166
except Exception as e:
152167
logger.error(f"Error creating AWS resource inventory: {str(e)}", exc_info=True)
@@ -172,6 +187,8 @@ def build_aws_cost_inventory(cloud_service_provider, provider_details, report_pa
172187
)
173188
cost_explorer = session.client('ce', region_name='us-east-1')
174189

190+
db_path = os.path.join(report_path, "data", "assessment.db")
191+
175192
end_time = date.today()
176193
start_time = end_time.replace(day=1) - timedelta(days=180)
177194
start_time = start_time.replace(day=1)
@@ -189,24 +206,54 @@ def build_aws_cost_inventory(cloud_service_provider, provider_details, report_pa
189206
}
190207
)
191208

192-
cost_inventory_raw_path = os.path.join(report_path, "cost_inventory_raw_data.json")
209+
cost_inventory_raw_path = os.path.join(raw_data_path, "cost_inventory_raw_data.json")
193210
with open(cost_inventory_raw_path, "w", encoding="utf-8") as raw_file:
194211
json.dump(cost_and_usage, raw_file, indent=4)
195212

196-
structured_costs = {}
197-
for result in cost_and_usage['ResultsByTime']:
198-
month_str = result['TimePeriod']['Start']
199-
total_cost = sum(float(group['Metrics']['UnblendedCost']['Amount']) for group in result['Groups'])
200-
currency = result['Groups'][0]['Metrics']['UnblendedCost']['Unit'] if result['Groups'] else 'USD'
201-
structured_costs[month_str] = {"cost": total_cost, "currency": currency}
202-
203-
missing_months = get_missing_months_aws(structured_costs.keys(), 6)
204-
for missing_month in missing_months:
205-
structured_costs[missing_month.isoformat()] = {"cost": 0.00, "currency": currency}
206-
207-
cost_inventory_standard_path = os.path.join(report_path, "cost_inventory_standard_data.json")
208-
with open(cost_inventory_standard_path, "w", encoding="utf-8") as structured_file:
209-
json.dump(structured_costs, structured_file, indent=4)
213+
# Insert structured data into SQLite
214+
with connect(db_path=db_path) as conn:
215+
cursor = conn.cursor()
216+
217+
for result in cost_and_usage['ResultsByTime']:
218+
month_str = result['TimePeriod']['Start']
219+
total_cost = sum(float(group['Metrics']['UnblendedCost']['Amount']) for group in result['Groups'])
220+
currency = result['Groups'][0]['Metrics']['UnblendedCost']['Unit'] if result['Groups'] else 'USD'
221+
month_date = datetime.strptime(month_str, '%Y-%m-%d').date().replace(day=1).isoformat()
222+
223+
# Insert or update the cost data for the month
224+
cursor.execute(
225+
"""
226+
INSERT INTO cost_inventory (month, cost, currency)
227+
VALUES (?, ?, ?)
228+
ON CONFLICT(month) DO UPDATE SET
229+
cost = excluded.cost,
230+
currency = excluded.currency
231+
""",
232+
(month_date, total_cost, currency)
233+
)
234+
235+
# Handle missing months
236+
structured_months = {datetime.strptime(result['TimePeriod']['Start'], '%Y-%m-%d').date() for result in cost_and_usage['ResultsByTime']}
237+
missing_months = get_missing_months_aws({month.isoformat() for month in structured_months}, 6)
238+
239+
for missing_month in missing_months:
240+
cursor.execute(
241+
"""
242+
INSERT INTO cost_inventory (month, cost, currency)
243+
VALUES (?, 0.00, ?)
244+
ON CONFLICT(month) DO UPDATE SET
245+
currency = excluded.currency
246+
""",
247+
(missing_month.isoformat(), currency)
248+
)
249+
250+
conn.commit()
251+
252+
except sqlite3.Error as e:
253+
logger.error(f"SQLite error: {str(e)}", exc_info=True)
254+
except Exception as e:
255+
logger.error(f"Error creating AWS cost inventory: {str(e)}", exc_info=True)
256+
raise
210257

211258
except Exception as e:
212259
logger.error(f"Error creating AWS cost inventory: {str(e)}", exc_info=True)

0 commit comments

Comments
 (0)