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.
@@ -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,4 @@
1
+ """
2
+ AWS integration module
3
+ Contains AWS SDK operations, SSO handling, and account management
4
+ """
@@ -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)