multi-aws-tool 0.1.1__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.
- multi_aws_tool/__init__.py +65 -0
- multi_aws_tool/aws/__init__.py +4 -0
- multi_aws_tool/aws/account_manager.py +335 -0
- multi_aws_tool/aws/sso_client.py +444 -0
- multi_aws_tool/cli/__init__.py +4 -0
- multi_aws_tool/cli/commands.py +1799 -0
- multi_aws_tool/config/__init__.py +4 -0
- multi_aws_tool/config/manager.py +250 -0
- multi_aws_tool/config/schema.py +199 -0
- multi_aws_tool/main.py +22 -0
- multi_aws_tool/models/__init__.py +4 -0
- multi_aws_tool/models/account.py +265 -0
- multi_aws_tool/models/config.py +173 -0
- multi_aws_tool/models/result.py +257 -0
- multi_aws_tool/output.py +476 -0
- multi_aws_tool/utils/__init__.py +4 -0
- multi_aws_tool/utils/account_data.py +365 -0
- multi_aws_tool/utils/data_validation.py +365 -0
- multi_aws_tool/utils/logging_config.py +90 -0
- multi_aws_tool/utils/report_parser.py +195 -0
- multi_aws_tool/utils/validators.py +229 -0
- multi_aws_tool-0.1.1.dist-info/METADATA +407 -0
- multi_aws_tool-0.1.1.dist-info/RECORD +25 -0
- multi_aws_tool-0.1.1.dist-info/WHEEL +4 -0
- multi_aws_tool-0.1.1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultiAWSTool - Multi-AWS Account Management Tool
|
|
3
|
+
A CLI application for managing AWS operations across multiple accounts via AWS SSO.
|
|
4
|
+
|
|
5
|
+
This package can be used as a library to manage AWS accounts and SSO authentication
|
|
6
|
+
in other Python projects.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.1"
|
|
10
|
+
__author__ = "MultiAWSTool Team"
|
|
11
|
+
__description__ = "Multi AWS tool for managing operations across multiple AWS accounts via SSO"
|
|
12
|
+
|
|
13
|
+
# Expose key classes for library usage
|
|
14
|
+
from .aws.account_manager import AccountManager, AccountManagerError
|
|
15
|
+
from .aws.sso_client import SSOClient, SSOAuthenticationError
|
|
16
|
+
from .config.manager import ConfigManager, ConfigurationError
|
|
17
|
+
from .models.account import Account, AccountCollection, Role, AccountStatus
|
|
18
|
+
from .models.config import MultiAWSConfig
|
|
19
|
+
from .models.result import CommandResult, ResultStatus, ExecutionSummary
|
|
20
|
+
from .utils.account_data import AccountDataManager, AccountDataError
|
|
21
|
+
|
|
22
|
+
# Output parsing and analysis
|
|
23
|
+
from .output import (
|
|
24
|
+
AccountResult,
|
|
25
|
+
MultiAWSExecutionSummary,
|
|
26
|
+
OutputParser,
|
|
27
|
+
OutputAnalyzer,
|
|
28
|
+
parse_execution_summary,
|
|
29
|
+
analyze_execution_summary
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Convenience imports for common use cases
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Main management classes
|
|
35
|
+
'AccountManager',
|
|
36
|
+
'SSOClient',
|
|
37
|
+
'ConfigManager',
|
|
38
|
+
|
|
39
|
+
# Data models
|
|
40
|
+
'Account',
|
|
41
|
+
'AccountCollection',
|
|
42
|
+
'Role',
|
|
43
|
+
'AccountStatus',
|
|
44
|
+
'MultiAWSConfig',
|
|
45
|
+
'CommandResult',
|
|
46
|
+
'ResultStatus',
|
|
47
|
+
'ExecutionSummary',
|
|
48
|
+
|
|
49
|
+
# Output parsing and analysis
|
|
50
|
+
'AccountResult',
|
|
51
|
+
'MultiAWSExecutionSummary',
|
|
52
|
+
'OutputParser',
|
|
53
|
+
'OutputAnalyzer',
|
|
54
|
+
'parse_execution_summary',
|
|
55
|
+
'analyze_execution_summary',
|
|
56
|
+
|
|
57
|
+
# Utility classes
|
|
58
|
+
'AccountDataManager',
|
|
59
|
+
|
|
60
|
+
# Exceptions
|
|
61
|
+
'AccountManagerError',
|
|
62
|
+
'SSOAuthenticationError',
|
|
63
|
+
'ConfigurationError',
|
|
64
|
+
'AccountDataError',
|
|
65
|
+
]
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Account management service for MultiAWSTool
|
|
3
|
+
Handles AWS account discovery, role enumeration, and account data management
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Optional, Dict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from .sso_client import SSOClient, SSOAuthenticationError
|
|
11
|
+
from ..models.account import Account, AccountCollection, AccountStatus, Role
|
|
12
|
+
from ..utils.account_data import AccountDataManager, AccountDataError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class AccountManagerError(Exception):
|
|
17
|
+
"""Raised when account management operations fail"""
|
|
18
|
+
|
|
19
|
+
class AccountManager:
|
|
20
|
+
"""Manages AWS accounts, roles, and their data persistence"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, sso_session_name: str = "default", region: str = "us-east-1",
|
|
23
|
+
account_file_path: Optional[str] = None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize account manager
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
sso_session_name: SSO session name for authentication
|
|
29
|
+
region: AWS region for SSO operations
|
|
30
|
+
account_file_path: Optional custom path for account data file
|
|
31
|
+
"""
|
|
32
|
+
self.sso_client = SSOClient(sso_session_name, region)
|
|
33
|
+
self.data_manager = AccountDataManager(account_file_path)
|
|
34
|
+
|
|
35
|
+
def discover_accounts(self, force_refresh: bool = False) -> AccountCollection:
|
|
36
|
+
"""
|
|
37
|
+
Discover AWS accounts through SSO and update local data
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
force_refresh: If True, force fresh discovery even if recently updated
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
AccountCollection with discovered accounts
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
AccountManagerError: If discovery fails
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
# Load existing accounts
|
|
50
|
+
existing_collection = self.data_manager.load_accounts()
|
|
51
|
+
|
|
52
|
+
# Check if we need to refresh
|
|
53
|
+
if not force_refresh and existing_collection.last_discovery:
|
|
54
|
+
# If discovered within last hour, use cached data
|
|
55
|
+
time_since_discovery = datetime.now() - existing_collection.last_discovery
|
|
56
|
+
if time_since_discovery.total_seconds() < 3600: # 1 hour
|
|
57
|
+
logger.info("Using cached account discovery (less than 1 hour old)")
|
|
58
|
+
return existing_collection
|
|
59
|
+
|
|
60
|
+
logger.info("Discovering AWS accounts through SSO...")
|
|
61
|
+
|
|
62
|
+
# Authenticate and discover accounts
|
|
63
|
+
accounts_data = self.sso_client.list_accounts()
|
|
64
|
+
|
|
65
|
+
# Get set of existing account IDs for comparison
|
|
66
|
+
existing_account_ids = {acc.id for acc in existing_collection.accounts}
|
|
67
|
+
discovered_account_ids = {acc_data['accountId'] for acc_data in accounts_data}
|
|
68
|
+
|
|
69
|
+
# Create new collection
|
|
70
|
+
new_collection = AccountCollection()
|
|
71
|
+
|
|
72
|
+
# Process discovered accounts
|
|
73
|
+
for account_data in accounts_data:
|
|
74
|
+
account_id = account_data['accountId']
|
|
75
|
+
account_name = account_data['accountName']
|
|
76
|
+
|
|
77
|
+
# Check if we have existing data for this account
|
|
78
|
+
existing_account = existing_collection.get_account(account_id)
|
|
79
|
+
|
|
80
|
+
if existing_account:
|
|
81
|
+
# Update existing account with new discovery info
|
|
82
|
+
existing_account.name = account_name # Update name in case it changed
|
|
83
|
+
existing_account.set_status(AccountStatus.ACTIVE) # Mark as active
|
|
84
|
+
existing_account.last_updated = datetime.now()
|
|
85
|
+
new_collection.add_account(existing_account)
|
|
86
|
+
logger.debug(f"Updated existing account: {account_id} ({account_name})")
|
|
87
|
+
else:
|
|
88
|
+
# Create new account
|
|
89
|
+
new_account = Account(
|
|
90
|
+
id=account_id,
|
|
91
|
+
name=account_name,
|
|
92
|
+
status=AccountStatus.ACTIVE
|
|
93
|
+
)
|
|
94
|
+
new_collection.add_account(new_account)
|
|
95
|
+
logger.info(f"Discovered new account: {account_id} ({account_name})")
|
|
96
|
+
|
|
97
|
+
# Mark accounts that are no longer accessible as disabled
|
|
98
|
+
disabled_accounts = existing_account_ids - discovered_account_ids
|
|
99
|
+
for account_id in disabled_accounts:
|
|
100
|
+
existing_account = existing_collection.get_account(account_id)
|
|
101
|
+
if existing_account and existing_account.status == AccountStatus.ACTIVE:
|
|
102
|
+
existing_account.set_status(AccountStatus.DISABLED)
|
|
103
|
+
new_collection.add_account(existing_account)
|
|
104
|
+
logger.warning(f"Account no longer accessible, marking as disabled: {account_id}")
|
|
105
|
+
|
|
106
|
+
# Update discovery timestamp
|
|
107
|
+
new_collection.update_discovery_time()
|
|
108
|
+
|
|
109
|
+
# Save updated collection
|
|
110
|
+
self.data_manager.save_accounts(new_collection)
|
|
111
|
+
|
|
112
|
+
active_count = len(new_collection.get_active_accounts())
|
|
113
|
+
disabled_count = len(new_collection.get_disabled_accounts())
|
|
114
|
+
logger.info(f"Account discovery complete: {active_count} active, {disabled_count} disabled")
|
|
115
|
+
|
|
116
|
+
return new_collection
|
|
117
|
+
|
|
118
|
+
except SSOAuthenticationError as e:
|
|
119
|
+
raise AccountManagerError(f"SSO authentication failed during account discovery: {e}") from e
|
|
120
|
+
except AccountDataError as e:
|
|
121
|
+
raise AccountManagerError(f"Failed to save account data: {e}") from e
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise AccountManagerError(f"Account discovery failed: {e}") from e
|
|
124
|
+
|
|
125
|
+
def discover_roles_for_account(self, account_id: str) -> List[Role]:
|
|
126
|
+
"""
|
|
127
|
+
Discover roles for a specific account
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
account_id: AWS account ID
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of discovered roles
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
AccountManagerError: If role discovery fails
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
logger.info(f"Discovering roles for account {account_id}...")
|
|
140
|
+
|
|
141
|
+
# Get roles from SSO
|
|
142
|
+
roles_data = self.sso_client.list_account_roles(account_id)
|
|
143
|
+
|
|
144
|
+
discovered_roles = []
|
|
145
|
+
for role_data in roles_data:
|
|
146
|
+
role_name = role_data['roleName']
|
|
147
|
+
# Construct ARN (SSO doesn't provide full ARN)
|
|
148
|
+
role_arn = f"arn:aws:iam::{account_id}:role/{role_name}"
|
|
149
|
+
|
|
150
|
+
role = Role(
|
|
151
|
+
name=role_name,
|
|
152
|
+
arn=role_arn,
|
|
153
|
+
description=f"Role discovered via SSO for account {account_id}"
|
|
154
|
+
)
|
|
155
|
+
discovered_roles.append(role)
|
|
156
|
+
logger.debug(f"Discovered role: {role_name}")
|
|
157
|
+
|
|
158
|
+
logger.info(f"Discovered {len(discovered_roles)} roles for account {account_id}")
|
|
159
|
+
return discovered_roles
|
|
160
|
+
|
|
161
|
+
except SSOAuthenticationError as e:
|
|
162
|
+
raise AccountManagerError(f"SSO authentication failed during role discovery: {e}") from e
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise AccountManagerError(f"Role discovery failed for account {account_id}: {e}") from e
|
|
165
|
+
|
|
166
|
+
def update_account_roles(self, account_id: str, force_refresh: bool = False) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Update roles for a specific account
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
account_id: AWS account ID
|
|
172
|
+
force_refresh: If True, force fresh role discovery
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if roles were updated, False if account not found
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
AccountManagerError: If role update fails
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
# Load accounts
|
|
182
|
+
collection = self.data_manager.load_accounts()
|
|
183
|
+
account = collection.get_account(account_id)
|
|
184
|
+
|
|
185
|
+
if not account:
|
|
186
|
+
logger.warning(f"Account {account_id} not found in local data")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
if not account.is_active():
|
|
190
|
+
logger.warning(f"Account {account_id} is disabled, skipping role update")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
# Check if we need to refresh roles
|
|
194
|
+
if not force_refresh and account.roles:
|
|
195
|
+
time_since_update = datetime.now() - account.last_updated
|
|
196
|
+
if time_since_update.total_seconds() < 1800: # 30 minutes
|
|
197
|
+
logger.info(f"Using cached roles for account {account_id} (updated recently)")
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
# Discover roles
|
|
201
|
+
discovered_roles = self.discover_roles_for_account(account_id)
|
|
202
|
+
|
|
203
|
+
# Update account with new roles
|
|
204
|
+
account.roles.clear() # Clear existing roles
|
|
205
|
+
for role in discovered_roles:
|
|
206
|
+
account.add_role(role)
|
|
207
|
+
|
|
208
|
+
account.last_updated = datetime.now()
|
|
209
|
+
|
|
210
|
+
# Save updated data
|
|
211
|
+
self.data_manager.save_accounts(collection)
|
|
212
|
+
|
|
213
|
+
logger.info(f"Updated {len(discovered_roles)} roles for account {account_id}")
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
except AccountDataError as e:
|
|
217
|
+
raise AccountManagerError(f"Failed to save role data: {e}") from e
|
|
218
|
+
|
|
219
|
+
def update_roles_for_accounts(self, account_ids: List[str], force_refresh: bool = False) -> Dict[str, bool]:
|
|
220
|
+
"""
|
|
221
|
+
Update roles for multiple accounts
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
account_ids: List of AWS account IDs
|
|
225
|
+
force_refresh: If True, force fresh role discovery
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dictionary mapping account ID to success status
|
|
229
|
+
"""
|
|
230
|
+
results = {}
|
|
231
|
+
|
|
232
|
+
for account_id in account_ids:
|
|
233
|
+
try:
|
|
234
|
+
success = self.update_account_roles(account_id, force_refresh)
|
|
235
|
+
results[account_id] = success
|
|
236
|
+
if success:
|
|
237
|
+
logger.info(f"Successfully updated roles for account {account_id}")
|
|
238
|
+
else:
|
|
239
|
+
logger.warning(f"Failed to update roles for account {account_id}")
|
|
240
|
+
except AccountManagerError as e:
|
|
241
|
+
logger.error(f"Error updating roles for account {account_id}: {e}")
|
|
242
|
+
results[account_id] = False
|
|
243
|
+
|
|
244
|
+
successful_updates = sum(1 for success in results.values() if success)
|
|
245
|
+
logger.info(f"Role update complete: {successful_updates}/{len(account_ids)} accounts updated")
|
|
246
|
+
|
|
247
|
+
return results
|
|
248
|
+
|
|
249
|
+
def get_accounts(self, status_filter: Optional[AccountStatus] = None) -> List[Account]:
|
|
250
|
+
"""
|
|
251
|
+
Get accounts with optional status filter
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
status_filter: Optional filter by account status
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of accounts
|
|
258
|
+
"""
|
|
259
|
+
collection = self.data_manager.load_accounts()
|
|
260
|
+
|
|
261
|
+
if status_filter:
|
|
262
|
+
return collection.get_accounts_by_status(status_filter)
|
|
263
|
+
else:
|
|
264
|
+
return list(collection.accounts)
|
|
265
|
+
|
|
266
|
+
def get_active_accounts(self) -> List[Account]:
|
|
267
|
+
"""Get all active accounts"""
|
|
268
|
+
return self.get_accounts(AccountStatus.ACTIVE)
|
|
269
|
+
|
|
270
|
+
def get_disabled_accounts(self) -> List[Account]:
|
|
271
|
+
"""Get all disabled accounts"""
|
|
272
|
+
return self.get_accounts(AccountStatus.DISABLED)
|
|
273
|
+
|
|
274
|
+
def get_account(self, account_id: str) -> Optional[Account]:
|
|
275
|
+
"""Get a specific account by ID"""
|
|
276
|
+
return self.data_manager.get_account(account_id)
|
|
277
|
+
|
|
278
|
+
def get_accounts_by_team(self, product_team: str) -> List[Account]:
|
|
279
|
+
"""
|
|
280
|
+
Get accounts belonging to a specific product team
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
product_team: Name of the product team
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of accounts in the specified team
|
|
287
|
+
"""
|
|
288
|
+
collection = self.data_manager.load_accounts()
|
|
289
|
+
return collection.get_accounts_by_product_team(product_team)
|
|
290
|
+
|
|
291
|
+
def get_accounts_with_role(self, role_name: str) -> List[Account]:
|
|
292
|
+
"""
|
|
293
|
+
Get accounts that have a specific role
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
role_name: Name of the role to search for
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of accounts with the specified role
|
|
300
|
+
"""
|
|
301
|
+
accounts = self.get_active_accounts()
|
|
302
|
+
return [account for account in accounts if account.has_role(role_name)]
|
|
303
|
+
|
|
304
|
+
def is_authenticated(self) -> bool:
|
|
305
|
+
"""Check if SSO is authenticated"""
|
|
306
|
+
return self.sso_client.is_authenticated()
|
|
307
|
+
|
|
308
|
+
def authenticate(self) -> bool:
|
|
309
|
+
"""Authenticate with SSO"""
|
|
310
|
+
try:
|
|
311
|
+
return self.sso_client.authenticate()
|
|
312
|
+
except SSOAuthenticationError as e:
|
|
313
|
+
logger.error(f"SSO authentication failed: {e}")
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
def logout(self) -> None:
|
|
317
|
+
"""Logout from SSO"""
|
|
318
|
+
self.sso_client.logout()
|
|
319
|
+
logger.info("Logged out from SSO")
|
|
320
|
+
|
|
321
|
+
# Convenience functions
|
|
322
|
+
def get_account_manager(sso_session_name: str = "default", region: str = "us-east-1",
|
|
323
|
+
account_file_path: Optional[str] = None) -> AccountManager:
|
|
324
|
+
"""Get an account manager instance"""
|
|
325
|
+
return AccountManager(sso_session_name, region, account_file_path)
|
|
326
|
+
|
|
327
|
+
def discover_accounts_quick(sso_session_name: str = "default", region: str = "us-east-1") -> AccountCollection:
|
|
328
|
+
"""Quick account discovery"""
|
|
329
|
+
manager = AccountManager(sso_session_name, region)
|
|
330
|
+
return manager.discover_accounts()
|
|
331
|
+
|
|
332
|
+
def get_available_roles(account_id: str, sso_session_name: str = "default", region: str = "us-east-1") -> List[Role]:
|
|
333
|
+
"""Get available roles for an account"""
|
|
334
|
+
manager = AccountManager(sso_session_name, region)
|
|
335
|
+
return manager.discover_roles_for_account(account_id)
|